diff --git a/src/pages/external/ExternalDetail.tsx b/src/pages/external/ExternalDetail.tsx index 6e9eb70..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; @@ -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'; @@ -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 d86a679..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(() => { @@ -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'; @@ -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 61d2577..ea20c30 100644 --- a/src/pages/issue/IssueDetail.tsx +++ b/src/pages/issue/IssueDetail.tsx @@ -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; @@ -290,7 +285,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -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, @@ -437,7 +456,7 @@ const IssueDetail = ({ initialMode }: IssueDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; @@ -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: [] }); } @@ -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)) { diff --git a/src/pages/workspace/WorkspaceExternalDetail.tsx b/src/pages/workspace/WorkspaceExternalDetail.tsx index 0ad7e87..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; @@ -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'; @@ -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 d69a0a5..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(() => { @@ -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'; @@ -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 bf41604..12191e3 100644 --- a/src/pages/workspace/WorkspaceIssueDetail.tsx +++ b/src/pages/workspace/WorkspaceIssueDetail.tsx @@ -121,18 +121,13 @@ 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 []; 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; @@ -290,7 +285,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { 우선순위: pr3, 없음: pr0, 낮음: pr1, - 중간: pr2, + 보통: pr2, 높음: pr3, 긴급: pr4, }; @@ -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, @@ -437,7 +456,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => {
e.stopPropagation()}> { const next = priorityLabelToCode[label] ?? 'NONE'; @@ -460,6 +479,7 @@ const WorkspaceIssueDetail = ({ initialMode }: WorkspaceIssueDetailProps) => { // 1) '없음'만 선택된 경우만 비우기 if (labels.length === 1 && labels[0] === '없음') { setManagersId([]); + setManagersShowNoneLabel(true); if (isCompleted && Number.isFinite(numericIssueId)) { updateIssue({ managersId: [] }); } @@ -525,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)) {