From 9d93dc7b5fd1e9d7069b29a06edc1e76e1e0965f Mon Sep 17 00:00:00 2001 From: HiJuwon Date: Fri, 22 Aug 2025 09:30:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?#200=20[FIX]=20priority=20label=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=EA=B3=BC=20=EB=A7=9E=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?'=EC=A4=91=EA=B0=84'=20=EC=86=8D=EC=84=B1=EC=9D=84=20'=EB=B3=B4?= =?UTF-8?q?=ED=86=B5'=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=EB=90=98=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/external/ExternalDetail.tsx | 4 ++-- src/pages/goal/GoalDetail.tsx | 4 ++-- src/pages/issue/IssueDetail.tsx | 4 ++-- src/pages/workspace/WorkspaceExternalDetail.tsx | 4 ++-- src/pages/workspace/WorkspaceGoalDetail.tsx | 4 ++-- src/pages/workspace/WorkspaceIssueDetail.tsx | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/external/ExternalDetail.tsx b/src/pages/external/ExternalDetail.tsx index 6e9eb70..c2639e4 100644 --- a/src/pages/external/ExternalDetail.tsx +++ b/src/pages/external/ExternalDetail.tsx @@ -403,7 +403,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -558,7 +558,7 @@ const ExternalDetail = ({ initialMode }: ExternalDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; diff --git a/src/pages/goal/GoalDetail.tsx b/src/pages/goal/GoalDetail.tsx index d86a679..1a50fd2 100644 --- a/src/pages/goal/GoalDetail.tsx +++ b/src/pages/goal/GoalDetail.tsx @@ -280,7 +280,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -421,7 +421,7 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; diff --git a/src/pages/issue/IssueDetail.tsx b/src/pages/issue/IssueDetail.tsx index 61d2577..12b4380 100644 --- a/src/pages/issue/IssueDetail.tsx +++ b/src/pages/issue/IssueDetail.tsx @@ -290,7 +290,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -437,7 +437,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; diff --git a/src/pages/workspace/WorkspaceExternalDetail.tsx b/src/pages/workspace/WorkspaceExternalDetail.tsx index 0ad7e87..443ea36 100644 --- a/src/pages/workspace/WorkspaceExternalDetail.tsx +++ b/src/pages/workspace/WorkspaceExternalDetail.tsx @@ -404,7 +404,7 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps) 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -559,7 +559,7 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps)
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; diff --git a/src/pages/workspace/WorkspaceGoalDetail.tsx b/src/pages/workspace/WorkspaceGoalDetail.tsx index d69a0a5..f4237c3 100644 --- a/src/pages/workspace/WorkspaceGoalDetail.tsx +++ b/src/pages/workspace/WorkspaceGoalDetail.tsx @@ -280,7 +280,7 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -422,7 +422,7 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; diff --git a/src/pages/workspace/WorkspaceIssueDetail.tsx b/src/pages/workspace/WorkspaceIssueDetail.tsx index bf41604..ec9b73c 100644 --- a/src/pages/workspace/WorkspaceIssueDetail.tsx +++ b/src/pages/workspace/WorkspaceIssueDetail.tsx @@ -290,7 +290,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -437,7 +437,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; From 1f7a6e1ac4bbdc89749e5c0b5b24f375ca234067 Mon Sep 17 00:00:00 2001 From: HiJuwon Date: Fri, 22 Aug 2025 09:52:32 +0900 Subject: [PATCH 2/3] =?UTF-8?q?#200=20[FIX]=20=EB=8B=B4=EB=8B=B9=EC=9E=90?= =?UTF-8?q?=20'=EC=97=86=EC=9D=8C'=20=EC=84=A0=ED=83=9D=20=EC=95=88=20?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/external/ExternalDetail.tsx | 3 +- src/pages/goal/GoalDetail.tsx | 174 +++++++++--------- src/pages/issue/IssueDetail.tsx | 3 +- .../workspace/WorkspaceExternalDetail.tsx | 3 +- src/pages/workspace/WorkspaceGoalDetail.tsx | 3 +- src/pages/workspace/WorkspaceIssueDetail.tsx | 3 +- 6 files changed, 98 insertions(+), 91 deletions(-) diff --git a/src/pages/external/ExternalDetail.tsx b/src/pages/external/ExternalDetail.tsx index c2639e4..9d1a310 100644 --- a/src/pages/external/ExternalDetail.tsx +++ b/src/pages/external/ExternalDetail.tsx @@ -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; @@ -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: [] }); } diff --git a/src/pages/goal/GoalDetail.tsx b/src/pages/goal/GoalDetail.tsx index 1a50fd2..126c79f 100644 --- a/src/pages/goal/GoalDetail.tsx +++ b/src/pages/goal/GoalDetail.tsx @@ -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(() => { @@ -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: [] }); } @@ -469,101 +470,102 @@ const GoalDetail = ({ initialMode }: GoalDetailProps) => { : selectedManagerLabels } /> -
- {/* (4) 기한 */} -
{ - 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" - > - {/* '기한' 속성 아이콘 */} - date -
- {/* '기한' 항목명 - 날짜 설정하면 반영됨 */} - {getDisplayText()} - {/* 달력 드롭다운 오픈 */} - {isDropdownOpen && dropdownContent?.name === 'date' && ( - { - setSelectedDate(date); - handleSelectDateAndPatch(date); // view 모드 시 즉시 PATCH - }} - /> - )} + + {/* (4) 기한 */} +
{ + 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" + > + {/* '기한' 속성 아이콘 */} + date +
+ {/* '기한' 항목명 - 날짜 설정하면 반영됨 */} + {getDisplayText()} + {/* 달력 드롭다운 오픈 */} + {isDropdownOpen && dropdownContent?.name === 'date' && ( + { + setSelectedDate(date); + handleSelectDateAndPatch(date); // view 모드 시 즉시 PATCH + }} + /> + )} +
-
- {/* (5) 이슈 */} -
e.stopPropagation()}> - { - if (labels.includes('없음')) { - setIssuesId([]); - setIssuesShowNoneLabel(true); + {/* (5) 이슈 */} +
e.stopPropagation()}> + { + 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 - } - /> + /> +
-
- {/* 작성 완료 버튼 : 상세페이지 mode 전환을 관리 */} - 0} - isCompleted={isCompleted} - isSaving={isSaving} - onToggle={handleCompletion} - /> + {/* 작성 완료 버튼 : 상세페이지 mode 전환을 관리 */} + 0} + isCompleted={isCompleted} + isSaving={isSaving} + onToggle={handleCompletion} + /> +
+
+ + {isModalOpen && modalContent?.name === 'leaveConfirm' && blocker?.state === 'blocked' && ( + + 작성을 그만두시겠습니까? +
+ 작성중인 내용은 저장되지 않습니다. + + } + buttonText="확인" + buttonColor="bg-primary-blue" + onClick={() => { + confirmedRef.current = true; // 확인 눌렀음 표시 + closeModal(); + setTimeout(() => { + // 2) 다음 틱에 이동(레이스 방지) + blocker.proceed(); + }, 0); + }} + /> + )}
-
- - {isModalOpen && modalContent?.name === 'leaveConfirm' && blocker?.state === 'blocked' && ( - - 작성을 그만두시겠습니까? -
- 작성중인 내용은 저장되지 않습니다. - - } - buttonText="확인" - buttonColor="bg-primary-blue" - onClick={() => { - confirmedRef.current = true; // 확인 눌렀음 표시 - closeModal(); - setTimeout(() => { - // 2) 다음 틱에 이동(레이스 방지) - blocker.proceed(); - }, 0); - }} - /> - )}
); }; diff --git a/src/pages/issue/IssueDetail.tsx b/src/pages/issue/IssueDetail.tsx index 12b4380..fe98f4b 100644 --- a/src/pages/issue/IssueDetail.tsx +++ b/src/pages/issue/IssueDetail.tsx @@ -132,7 +132,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { 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; @@ -460,6 +460,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { // 1) '없음'만 선택된 경우만 비우기 if (labels.length === 1 && labels[0] === '없음') { setManagersId([]); + setManagersShowNoneLabel(true); if (isCompleted && Number.isFinite(numericIssueId)) { updateIssue({ managersId: [] }); } diff --git a/src/pages/workspace/WorkspaceExternalDetail.tsx b/src/pages/workspace/WorkspaceExternalDetail.tsx index 443ea36..03b9333 100644 --- a/src/pages/workspace/WorkspaceExternalDetail.tsx +++ b/src/pages/workspace/WorkspaceExternalDetail.tsx @@ -188,7 +188,7 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps) 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; @@ -582,6 +582,7 @@ const WorkspaceExternalDetail = ({ initialMode }: WorkspaceExternalDetailProps) // 1) '없음'만 선택된 경우만 비우기 if (labels.length === 1 && labels[0] === '없음') { setManagersId([]); + setManagersShowNoneLabel(true); if (isCompleted && Number.isFinite(numericExternalId)) { updateExternal({ managersId: [] }); } diff --git a/src/pages/workspace/WorkspaceGoalDetail.tsx b/src/pages/workspace/WorkspaceGoalDetail.tsx index f4237c3..978e851 100644 --- a/src/pages/workspace/WorkspaceGoalDetail.tsx +++ b/src/pages/workspace/WorkspaceGoalDetail.tsx @@ -128,7 +128,7 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { 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(() => { @@ -445,6 +445,7 @@ const WorkspaceGoalDetail = ({ initialMode }: WorkspaceGoalDetailProps) => { // 1) '없음'만 선택된 경우만 비우기 if (labels.length === 1 && labels[0] === '없음') { setManagersId([]); + setManagersShowNoneLabel(true); if (isCompleted && Number.isFinite(numericGoalId)) { updateGoal({ managersId: [] }); } diff --git a/src/pages/workspace/WorkspaceIssueDetail.tsx b/src/pages/workspace/WorkspaceIssueDetail.tsx index ec9b73c..23023f1 100644 --- a/src/pages/workspace/WorkspaceIssueDetail.tsx +++ b/src/pages/workspace/WorkspaceIssueDetail.tsx @@ -132,7 +132,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { 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; @@ -460,6 +460,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { // 1) '없음'만 선택된 경우만 비우기 if (labels.length === 1 && labels[0] === '없음') { setManagersId([]); + setManagersShowNoneLabel(true); if (isCompleted && Number.isFinite(numericIssueId)) { updateIssue({ managersId: [] }); } From fcf8a1b2222c46159fc10e2db859af52f1bbe05e Mon Sep 17 00:00:00 2001 From: HiJuwon Date: Fri, 22 Aug 2025 10:39:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?#200=20[FIX]=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=A4=91=EB=B3=B5=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/issue/IssueDetail.tsx | 51 +++++++++++++------- src/pages/workspace/WorkspaceIssueDetail.tsx | 51 +++++++++++++------- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/pages/issue/IssueDetail.tsx b/src/pages/issue/IssueDetail.tsx index fe98f4b..ea20c30 100644 --- a/src/pages/issue/IssueDetail.tsx +++ b/src/pages/issue/IssueDetail.tsx @@ -121,11 +121,6 @@ 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 []; @@ -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(); + 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, @@ -526,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)) { diff --git a/src/pages/workspace/WorkspaceIssueDetail.tsx b/src/pages/workspace/WorkspaceIssueDetail.tsx index 23023f1..12191e3 100644 --- a/src/pages/workspace/WorkspaceIssueDetail.tsx +++ b/src/pages/workspace/WorkspaceIssueDetail.tsx @@ -121,11 +121,6 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { 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 []; @@ -329,16 +324,40 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { return base; }, [teamMembers]); - const goalOptions = useMemo( - () => ['없음', ...(simpleGoals ?? []).map((g) => g.title)], - [simpleGoals] + const goals = simpleGoals ?? []; + + // 1) 제목별 개수 집계 + const goalTitleCount = useMemo(() => { + const m = new Map(); + 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, @@ -526,13 +545,11 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { // '없음' 대응 (백엔드가 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)) {