Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/pages/external/ExternalDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => {
const idToName = new Map(workspaceMembers.map((m) => [m.memberId, m.name] as const));
return managersId.map((id) => idToName.get(id)).filter((v): v is string => !!v);
}, [managersId, workspaceMembers]);
const [managersShowNoneLabel] = useState(false);
const [managersShowNoneLabel, setManagersShowNoneLabel] = useState(false);

useEffect(() => {
if (!isEditable) return;
Expand Down Expand Up @@ -403,7 +403,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => {
우선순위: pr3,
없음: pr0,
낮음: pr1,
중간: pr2,
보통: pr2,
높음: pr3,
긴급: pr4,
};
Expand Down Expand Up @@ -558,7 +558,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => {
<div onClick={(e) => e.stopPropagation()}>
<PropertyItem
defaultValue="우선순위"
options={['없음', '긴급', '높음', '중간', '낮음']}
options={['없음', '긴급', '높음', '보통', '낮음']}
iconMap={priorityIconMap}
onSelect={(label) => {
const next = priorityLabelToCode[label] ?? 'NONE';
Expand All @@ -581,6 +581,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => {
// 1) '없음'만 선택된 경우만 비우기
if (labels.length === 1 && labels[0] === '없음') {
setManagersId([]);
setManagersShowNoneLabel(true);
if (isCompleted && Number.isFinite(numericExternalId)) {
updateExternal({ managersId: [] });
}
Expand Down
178 changes: 90 additions & 88 deletions src/pages/goal/GoalDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
const idToTitle = new Map((simpleIssues ?? []).map((i) => [i.id, i.title] as const));
return issuesId.map((id) => idToTitle.get(id)).filter((v): v is string => !!v);
}, [issuesId, simpleIssues]);
const [managersShowNoneLabel] = useState(false);
const [managersShowNoneLabel, setManagersShowNoneLabel] = useState(false);
const [issuesShowNoneLabel, setIssuesShowNoneLabel] = useState(false);

useEffect(() => {
Expand Down Expand Up @@ -280,7 +280,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
우선순위: pr3,
없음: pr0,
낮음: pr1,
중간: pr2,
보통: pr2,
높음: pr3,
긴급: pr4,
};
Expand Down Expand Up @@ -421,7 +421,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
<div onClick={(e) => e.stopPropagation()}>
<PropertyItem
defaultValue="우선순위"
options={['없음', '긴급', '높음', '중간', '낮음']}
options={['없음', '긴급', '높음', '보통', '낮음']}
iconMap={priorityIconMap}
onSelect={(label) => {
const next = priorityLabelToCode[label] ?? 'NONE';
Expand All @@ -443,6 +443,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
// 1) '없음'만 선택된 경우만 비우기
if (labels.length === 1 && labels[0] === '없음') {
setManagersId([]);
setManagersShowNoneLabel(true);
if (isCompleted && Number.isFinite(numericGoalId)) {
updateGoal({ managersId: [] });
}
Expand All @@ -469,101 +470,102 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
: selectedManagerLabels
}
/>
</div>
{/* (4) 기한 */}
<div
onClick={(e) => {
e.stopPropagation();
openDropdown({ name: 'date' });
}}
className="flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer"
>
{/* '기한' 속성 아이콘 */}
<img src={IcCalendar} alt="date" />
<div className="relative">
{/* '기한' 항목명 - 날짜 설정하면 반영됨 */}
<span className={`font-body-r text-gray-600`}>{getDisplayText()}</span>
{/* 달력 드롭다운 오픈 */}
{isDropdownOpen && dropdownContent?.name === 'date' && (
<CalendarDropdown
selectedDate={selectedDate}
onSelect={(date) => {
setSelectedDate(date);
handleSelectDateAndPatch(date); // view 모드 시 즉시 PATCH
}}
/>
)}

{/* (4) 기한 */}
<div
onClick={(e) => {
e.stopPropagation();
openDropdown({ name: 'date' });
}}
className="flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer"
>
{/* '기한' 속성 아이콘 */}
<img src={IcCalendar} alt="date" />
<div className="relative">
{/* '기한' 항목명 - 날짜 설정하면 반영됨 */}
<span className={`font-body-r text-gray-600`}>{getDisplayText()}</span>
{/* 달력 드롭다운 오픈 */}
{isDropdownOpen && dropdownContent?.name === 'date' && (
<CalendarDropdown
selectedDate={selectedDate}
onSelect={(date) => {
setSelectedDate(date);
handleSelectDateAndPatch(date); // view 모드 시 즉시 PATCH
}}
/>
)}
</div>
</div>
</div>

{/* (5) 이슈 */}
<div onClick={(e) => e.stopPropagation()}>
<MultiSelectPropertyItem
defaultValue="이슈"
options={issueOptions}
onChange={(labels) => {
if (labels.includes('없음')) {
setIssuesId([]);
setIssuesShowNoneLabel(true);
{/* (5) 이슈 */}
<div onClick={(e) => e.stopPropagation()}>
<MultiSelectPropertyItem
defaultValue="이슈"
options={issueOptions}
onChange={(labels) => {
if (labels.includes('없음')) {
setIssuesId([]);
setIssuesShowNoneLabel(true);
if (isCompleted && Number.isFinite(numericGoalId)) {
updateGoal({ issuesId: [] });
}
return;
}
const ids = labels
.map((label) => issueTitleToId[label])
.filter((v): v is number => typeof v === 'number');
setIssuesId(ids);
setIssuesShowNoneLabel(false);
if (isCompleted && Number.isFinite(numericGoalId)) {
updateGoal({ issuesId: [] });
updateGoal({ issuesId: ids });
}
return;
}
const ids = labels
.map((label) => issueTitleToId[label])
.filter((v): v is number => typeof v === 'number');
setIssuesId(ids);
setIssuesShowNoneLabel(false);
if (isCompleted && Number.isFinite(numericGoalId)) {
updateGoal({ issuesId: ids });
}}
selected={
issuesId.length === 0
? issuesShowNoneLabel
? ['없음']
: []
: selectedIssueLabels
}
}}
selected={
issuesId.length === 0
? issuesShowNoneLabel
? ['없음']
: []
: selectedIssueLabels
}
/>
/>
</div>
</div>
</div>
</div>

{/* 작성 완료 버튼 : 상세페이지 mode 전환을 관리 */}
<CompletionButton
isTitleFilled={title.trim().length > 0}
isCompleted={isCompleted}
isSaving={isSaving}
onToggle={handleCompletion}
/>
{/* 작성 완료 버튼 : 상세페이지 mode 전환을 관리 */}
<CompletionButton
isTitleFilled={title.trim().length > 0}
isCompleted={isCompleted}
isSaving={isSaving}
onToggle={handleCompletion}
/>
</div>
</div>
<div ref={bottomRef} className="scroll-mb-[6.4rem]" />

{isModalOpen && modalContent?.name === 'leaveConfirm' && blocker?.state === 'blocked' && (
<Modal
title="알림"
subtitle={
<>
작성을 그만두시겠습니까?
<br />
작성중인 내용은 저장되지 않습니다.
</>
}
buttonText="확인"
buttonColor="bg-primary-blue"
onClick={() => {
confirmedRef.current = true; // 확인 눌렀음 표시
closeModal();
setTimeout(() => {
// 2) 다음 틱에 이동(레이스 방지)
blocker.proceed();
}, 0);
}}
/>
)}
</div>
<div ref={bottomRef} className="scroll-mb-[6.4rem]" />

{isModalOpen && modalContent?.name === 'leaveConfirm' && blocker?.state === 'blocked' && (
<Modal
title="알림"
subtitle={
<>
작성을 그만두시겠습니까?
<br />
작성중인 내용은 저장되지 않습니다.
</>
}
buttonText="확인"
buttonColor="bg-primary-blue"
onClick={() => {
confirmedRef.current = true; // 확인 눌렀음 표시
closeModal();
setTimeout(() => {
// 2) 다음 틱에 이동(레이스 방지)
blocker.proceed();
}, 0);
}}
/>
)}
</div>
);
};
Expand Down
58 changes: 38 additions & 20 deletions src/pages/issue/IssueDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,13 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
const selectedStatusLabel = STATUS_LABELS[state];
const selectedPriorityLabel = PRIORITY_LABELS[priority];

const selectedGoalLabel = useMemo(() => {
const match = (simpleGoals ?? []).find((g) => g.id === goalId);
return match?.title ?? '목표'; // 데이터 없거나 매칭 실패 시 기본 라벨
}, [simpleGoals, goalId]);

// 다중 선택 라벨
const selectedManagerLabels = useMemo(() => {
if (!workspaceMembers) return [];
const idToName = new Map(workspaceMembers.map((m) => [m.memberId, m.name] as const));
return managersId.map((id) => idToName.get(id)).filter((v): v is string => !!v);
}, [managersId, workspaceMembers]);
const [managersShowNoneLabel] = useState(false);
const [managersShowNoneLabel, setManagersShowNoneLabel] = useState(false);

useEffect(() => {
if (!isEditable) return;
Expand Down Expand Up @@ -290,7 +285,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
우선순위: pr3,
없음: pr0,
낮음: pr1,
중간: pr2,
보통: pr2,
높음: pr3,
긴급: pr4,
};
Expand Down Expand Up @@ -329,16 +324,40 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
return base;
}, [teamMembers]);

const goalOptions = useMemo(
() => ['없음', ...(simpleGoals ?? []).map((g) => g.title)],
[simpleGoals]
const goals = simpleGoals ?? [];

// 1) 제목별 개수 집계
const goalTitleCount = useMemo(() => {
const m = new Map<string, number>();
for (const g of goals) m.set(g.title, (m.get(g.title) ?? 0) + 1);
return m;
}, [goals]);

// 2) 표시용 라벨 구성 (중복이면 #id suffix)
const goalItems = useMemo(
() =>
goals.map((g) => {
const duplicated = (goalTitleCount.get(g.title) ?? 0) > 1;
const label = duplicated ? `${g.title} (#${g.id})` : g.title;
return { id: g.id, label };
}),
[goals, goalTitleCount]
);

// 3) 드롭다운 옵션 배열
const goalOptions = useMemo(() => ['없음', ...goalItems.map((o) => o.label)], [goalItems]);

// 4) 라벨 → id 역매핑
const goalLabelToId = useMemo(
() => new Map(goalItems.map((o) => [o.label, o.id] as const)),
[goalItems]
);

// title -> id 역매핑
const goalTitleToId = useMemo(() => {
const info = simpleGoals ?? [];
return new Map(info.map((g) => [g.title, g.id] as const));
}, [simpleGoals]);
const selectedGoalLabel = useMemo(() => {
if (goalId == null) return '목표';
const item = goalItems.find((o) => o.id === goalId);
return item?.label ?? '목표';
}, [goalId, goalItems]);

useHydrateIssueDetail({
issueDetail,
Expand Down Expand Up @@ -437,7 +456,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
<div onClick={(e) => e.stopPropagation()}>
<PropertyItem
defaultValue="우선순위"
options={['없음', '긴급', '높음', '중간', '낮음']}
options={['없음', '긴급', '높음', '보통', '낮음']}
iconMap={priorityIconMap}
onSelect={(label) => {
const next = priorityLabelToCode[label] ?? 'NONE';
Expand All @@ -460,6 +479,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
// 1) '없음'만 선택된 경우만 비우기
if (labels.length === 1 && labels[0] === '없음') {
setManagersId([]);
setManagersShowNoneLabel(true);
if (isCompleted && Number.isFinite(numericIssueId)) {
updateIssue({ managersId: [] });
}
Expand Down Expand Up @@ -525,13 +545,11 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
// '없음' 대응 (백엔드가 null 허용 전이라면 0으로)
if (label === '없음') {
setGoalId(null);
if (isCompleted && Number.isFinite(numericIssueId)) {
}
return;
}

// title -> id 매핑
const id = goalTitleToId.get(label);
// label -> id 매핑
const id = goalLabelToId.get(label);
if (typeof id === 'number') {
setGoalId(id);
if (isCompleted && Number.isFinite(numericIssueId)) {
Expand Down
Loading