(null);
@@ -695,8 +704,12 @@ function OvertimeSection({ isReviewer }: { isReviewer: boolean }) {
setSaving(true);
try {
const extendedEnd = `${date}T${endTime}:00+09:00`;
- await api("/api/attendance/overtime", { method: "POST", json: { date, extendedEnd, reason: reason || undefined } });
+ await api("/api/attendance/overtime", {
+ method: "POST",
+ json: { date, extendedEnd, reason: reason || undefined, companions: mates.length ? mates.map((m) => m.id) : undefined },
+ });
setReason("");
+ setMates([]);
await load();
} catch (e: any) {
alertAsync({ title: "신청 실패", description: e?.message ?? "야근 신청에 실패했어요" });
@@ -721,11 +734,36 @@ function OvertimeSection({ isReviewer }: { isReviewer: boolean }) {
reason: o.reason,
createdAt: o.createdAt,
companyName,
+ companions: (o.companions ?? []).map((c) => c.name),
});
} catch (e: any) {
alertAsync({ title: "PDF 생성 실패", description: e?.message ?? "신청서 PDF 생성에 실패했어요" });
} finally { setPdfBusy(null); }
}
+ async function openMatePicker() {
+ const r = mateBtnRef.current?.getBoundingClientRect();
+ if (r) setMateRect({ top: Math.round(r.bottom + 6), left: Math.round(Math.max(8, Math.min(r.left, window.innerWidth - 276))) });
+ setMateQuery("");
+ setMateOpen(true);
+ if (!mateUsers) {
+ try {
+ const res = await api<{ users: MateLite[] }>("/api/users");
+ setMateUsers(res.users ?? []);
+ } catch { setMateUsers([]); }
+ }
+ }
+ // 바깥 클릭으로 피커 닫기 — 팝오버(.otmate-pop) 안 클릭은 유지.
+ useEffect(() => {
+ if (!mateOpen) return;
+ function onDoc(e: MouseEvent) {
+ const t = e.target as HTMLElement;
+ if (t.closest(".otmate-pop") || t === mateBtnRef.current) return;
+ setMateOpen(false);
+ }
+ document.addEventListener("mousedown", onDoc);
+ return () => document.removeEventListener("mousedown", onDoc);
+ }, [mateOpen]);
+
// 계획내용 입력 제한 — PDF 계획 박스(A4 1페이지 고정 330px)에 실제로 들어가는 줄 수까지만.
// 자동 줄바꿈 포함 실측(measurePlanLines)이라 "몇 자"가 아니라 "몇 줄"이 기준. 넘치면
// 들어가는 만큼으로 잘라(clampPlanText) 서식이 2페이지로 넘어가거나 글이 잘리는 일이 없다.
@@ -756,6 +794,7 @@ function OvertimeSection({ isReviewer }: { isReviewer: boolean }) {
reason,
createdAt: new Date().toISOString(),
companyName,
+ companions: mates.map((m) => m.name),
});
} catch (e: any) {
alertAsync({ title: "PDF 생성 실패", description: e?.message ?? "신청서 PDF 생성에 실패했어요" });
@@ -786,6 +825,19 @@ function OvertimeSection({ isReviewer }: { isReviewer: boolean }) {
까지 (휴게시간 제외)
+ 함께 근무
+
+ {mates.map((m) => (
+
+ {m.name}
+
+
+ ))}
+ {mates.length < 5 && (
+
+ )}
+ {mates.length === 0 && (선택) 같이 야근하는 동료}
+
계획 내용 (업무 내용)
= planLines.maxLines ? "otform-lines-full" : ""}`}>
@@ -804,6 +856,48 @@ function OvertimeSection({ isReviewer }: { isReviewer: boolean }) {
+ {mateOpen && mateRect && (
+
+
+
setMateQuery(e.target.value)}
+ />
+
+ {mateUsers === null ? (
+
불러오는 중…
+ ) : (
+ (() => {
+ const q = mateQuery.trim().toLowerCase();
+ const picked = new Set(mates.map((m) => m.id));
+ const items = (mateUsers ?? [])
+ .filter((m) => m.id !== user?.id && !picked.has(m.id))
+ .filter((m) => !q || m.name.toLowerCase().includes(q) || (m.team ?? "").toLowerCase().includes(q))
+ .slice(0, 30);
+ if (!items.length) return
검색 결과가 없어요
;
+ return items.map((m) => (
+
+ ));
+ })()
+ )}
+
+
+
+ )}