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"
- >
- {/* '기한' 속성 아이콘 */}
-

-
- {/* '기한' 항목명 - 날짜 설정하면 반영됨 */}
-
{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"
+ >
+ {/* '기한' 속성 아이콘 */}
+

+
+ {/* '기한' 항목명 - 날짜 설정하면 반영됨 */}
+ {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)) {