diff --git a/apps/web/README.md b/apps/web/README.md
index dedc096..179e6eb 100644
--- a/apps/web/README.md
+++ b/apps/web/README.md
@@ -95,10 +95,10 @@ await showChangeNotification({
| -------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| A2 목록 | "확인할 변경"(대기)과 "확인한 변경"(답함, 대체됨), "전체"/"이슈 별로 보기" 탭, 빈 화면, 알림 꺼짐 안내, 불러오기 실패 이유 | `api.listMyChanges(me)` 한 번 (notify.list.one-call) |
| A3 상세·답하기 | 결정 사항, 이전 값 → 지금 값, 이유, 출처 칩, 누가·언제, 내 이슈, 관계있다/없다 → 알았다/몰랐다 또는 이유(100자) | `api.answer(me, input)` (채널 `'app'`) |
-| A4 답한 뒤 | 내 답과 답한 시각, 진행 3단계(새 변경 → 확인 완료 → 작업 반영), "작업 반영했어요" 체크("관계있다"로 답한 카드만) | `api.markReflected(me, id, reflected)` |
+| A4 답한 뒤 | 내 답과 답한 시각, 진행 3단계(결정 사항 변경 → 확인 완료 → 이슈 반영), "이슈에 반영했어요" 체크("관계있다"로 답한 카드만) | `api.markReflected(me, id, reflected)` |
| A1 OS 알림 | 창이 열려 있는 동안 새 대기 변경이 생기면 알림(제목 40자 이하), 누르면 그 변경의 상세 | `api.watchMyChanges(me, …)` → `showChangeNotification` |
-- **화면 모양:** 신지님 프로토타입(https://suju0.github.io/haribo-web/)의 크기·여백·글자·색을 그대로 따라요. 값은 `src/ui/`와 각 화면 파일에 있고, 색은 `styles.css`의 TDS 색 변수예요. 알림 꺼짐 안내의 설정 방법 그림은 `public/notify-guide.svg`(프로토타입 그림)예요.
+- **화면 모양:** 신지님 프로토타입(https://suju0.github.io/haribo-web/)의 크기·여백·글자·색을 그대로 따라요. 값은 `src/ui/`와 각 화면 파일에 있고, 색은 `styles.css`의 TDS 색 변수예요. 알림 꺼짐 안내의 설정 방법 그림은 `public/notify-step-1~3.svg`(프로토타입 그림)이고, 그림 아래 글자는 진짜 텍스트예요.
- **내 이름:** 주소에 `?me=<이름>`(이슈 담당자 이름)을 붙여 한 번 열면 브라우저가 기억해요. 예시 데이터로 열면 이름 없이도 예시를 받는 사람(민수)으로 열려요. 이름을 고르는 화면은 없어요 (10/15 호윤).
- **예시 데이터로 확인:** `pnpm app:web` → 데모 예시(`app.notify.title-length` 40자 → 32자) 카드의 "답하기"를 눌러 답하고 반영 체크까지 해 봐요. 답은 창 메모리에만 저장돼요 (새로 고치면 처음으로).
- **목록은 터미널과 같은 함수로 만들어요:** `@sil/notify/browser`의 `loadNotifyData` + `buildPendingList`예요 (SIL-100). 자동 닫힘 숨기기, 답함·대체됨 7일 뒤 숨기기, 지난번 답 안내(`previousAnswer`)를 그 함수가 채워요. 예시 데이터도 같은 함수를 거쳐요 (날짜는 10/15로 고정).
diff --git a/apps/web/public/notify-guide.svg b/apps/web/public/notify-guide.svg
deleted file mode 100644
index 9058a56..0000000
--- a/apps/web/public/notify-guide.svg
+++ /dev/null
@@ -1,125 +0,0 @@
-
diff --git a/apps/web/public/notify-step-1.svg b/apps/web/public/notify-step-1.svg
new file mode 100644
index 0000000..c5cef5a
--- /dev/null
+++ b/apps/web/public/notify-step-1.svg
@@ -0,0 +1,39 @@
+
diff --git a/apps/web/public/notify-step-2.svg b/apps/web/public/notify-step-2.svg
new file mode 100644
index 0000000..f5f75d2
--- /dev/null
+++ b/apps/web/public/notify-step-2.svg
@@ -0,0 +1,43 @@
+
diff --git a/apps/web/public/notify-step-3.svg b/apps/web/public/notify-step-3.svg
new file mode 100644
index 0000000..971297f
--- /dev/null
+++ b/apps/web/public/notify-step-3.svg
@@ -0,0 +1,45 @@
+
diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts
index e40ed26..c0facba 100644
--- a/apps/web/src/api/types.ts
+++ b/apps/web/src/api/types.ts
@@ -51,7 +51,7 @@ export type MyChangeStandard = PendingItemStandard;
export type MyChangeIssue = PendingItemIssue;
/** 내가 한 답 (A4). 대기·대체됨이면 MyChange.answer가 null이에요 */
export type MyChangeAnswer = PendingItemAnswer;
-/** 같은 이슈에서 이전 버전에 한 마지막 답. "관계있다"였으면 "다시 고쳐야 해요" 안내를 붙여요 */
+/** 같은 이슈에서 이전 버전에 한 마지막 답. "관계있다"였으면 "최신 결정 사항을 반영해 주세요" 안내를 붙여요 (notify.pending.renotify-hint v2) */
export type MyChangePreviousAnswer = PreviousAnswer;
/** 나에게 온 변경에 답하기. 답한 사람과 답한 곳('app')은 api가 채워요 */
@@ -69,7 +69,7 @@ export interface SilApi {
listMyChanges(me: string): Promise;
/** 답하기. 이미 닫혔으면 PENDING_CLOSED, 받은 사람이 아니면 FORBIDDEN, 이유가 비었거나 길면 VALIDATION_FAILED */
answer(me: string, input: MyAnswerInput): Promise;
- /** 작업 반영함 켜기·끄기. 답한 변경에만 돼요. 풀어도 이슈를 다시 열지 않아요 (notify.check.reflected) */
+ /** 이슈 반영함 켜기·끄기. 답한 변경에만 돼요. 풀어도 이슈를 다시 열지 않아요 (notify.check.reflected) */
markReflected(
me: string,
pendingCheckId: string,
diff --git a/apps/web/src/screens/MyChangesScreen.tsx b/apps/web/src/screens/MyChangesScreen.tsx
index 8632485..ae7e1da 100644
--- a/apps/web/src/screens/MyChangesScreen.tsx
+++ b/apps/web/src/screens/MyChangesScreen.tsx
@@ -175,8 +175,7 @@ function MyChanges(props: ScreenProps & { me: string }) {
나에게 온 변경 목록
- {props.me}님 이슈에 걸린 결정 사항이 바뀌면 여기에 와요. 확인한
- 변경은 7일 동안 보여요.
+ 답한 변경은 7일 뒤에 목록에서 내려가요
diff --git a/apps/web/src/screens/my-changes/Badges.tsx b/apps/web/src/screens/my-changes/Badges.tsx
index 041c2c1..a0fd74f 100644
--- a/apps/web/src/screens/my-changes/Badges.tsx
+++ b/apps/web/src/screens/my-changes/Badges.tsx
@@ -1,5 +1,5 @@
/**
- * 카드와 상세에 붙는 칩이에요: 상태, 새 결정 사항, 새 버전, 되돌림, 출처, PR에서 찾음, 완료된 이슈.
+ * 카드와 상세에 붙는 칩이에요: 상태, 새 결정 사항, 새 버전, 되돌림, 출처(PR에서 찾은 변경 포함), 완료된 이슈.
* 색과 크기는 프로토타입(SIL-69)과 같아요.
*/
import type { MyChange } from '../../api/types.ts';
@@ -43,8 +43,8 @@ export function StatusChip(props: { change: MyChange }) {
);
}
-/** 새 결정 사항, 새 버전, 되돌림 */
-export function KindChips(props: { change: MyChange }) {
+/** 새 결정 사항, 새 버전, 되돌림. 상세 제목 옆에서는 되돌림을 빼고 "현재 결정 사항" 상자에 붙여요 (신지님 시안 10/15) */
+export function KindChips(props: { change: MyChange; hideRevert?: boolean }) {
const { change } = props;
return (
<>
@@ -58,7 +58,7 @@ export function KindChips(props: { change: MyChange }) {
새 버전 v{change.standard.version}
)}
- {change.standard.isRevert && (
+ {change.standard.isRevert && !props.hideRevert && (
되돌림
@@ -67,25 +67,16 @@ export function KindChips(props: { change: MyChange }) {
);
}
-/** 출처 칩과 "PR에서 찾음" (app.card.source-chip-only, app.card.found-in-pr-code-only) */
+/**
+ * 출처 칩 하나. 모두 같은 모양이고 AI 대화에만 반짝 아이콘을 붙여요 (신지님 시안 10/15)
+ * (app.card.source-chip-only, app.card.found-in-pr-code-only v2)
+ */
export function SourceChips(props: { change: MyChange }) {
const { change } = props;
+ const isAiChat = change.standard.source === 'ai_chat' && !isFoundInPr(change);
return (
- <>
- {change.standard.source === 'ai_chat' ? (
- }>
- {sourceChipLabel(change)}
-
- ) : (
-
- {sourceChipLabel(change)}
-
- )}
- {isFoundInPr(change) && (
-
- PR에서 찾음
-
- )}
- >
+ : undefined}>
+ {sourceChipLabel(change)}
+
);
}
diff --git a/apps/web/src/screens/my-changes/ChangeDetail.tsx b/apps/web/src/screens/my-changes/ChangeDetail.tsx
index 9ed4eed..8616a75 100644
--- a/apps/web/src/screens/my-changes/ChangeDetail.tsx
+++ b/apps/web/src/screens/my-changes/ChangeDetail.tsx
@@ -4,11 +4,11 @@
* - 답: 관계있다/관계없다 → 알았다/몰랐다(누르면 바로 저장) 또는 관계없는 이유(100자). 관계없다면 known을 보내지 않아요.
* - 답할 때 에러: PENDING_CLOSED는 에러 없이 조용히 닫고(notify.answer.one-place), FORBIDDEN이면 답하기를 숨기고,
* VALIDATION_FAILED는 입력란 아래에 보여 줘요 (타입명세서 11-4).
- * - 진행 단계는 새 변경 → 확인 완료 → 작업 반영 세 개예요 (app.card.three-steps).
- * - "작업 반영했어요"는 "관계있다"로 답한 카드에만, 답한 뒤에만 켜요. 풀어도 이슈를 다시 열지 않아요 (notify.check.reflected).
+ * - 진행 단계는 결정 사항 변경 → 확인 완료 → 이슈 반영 세 개예요 (app.card.three-steps v3).
+ * - "이슈에 반영했어요"는 "관계있다"로 답한 카드에만, 답한 뒤에만 켜요. 풀어도 이슈를 다시 열지 않아요 (notify.check.reflected).
*/
import { isSilError, MAX_UNAFFECTED_REASON_LENGTH } from '@sil/core';
-import { useRef, useState, type CSSProperties } from 'react';
+import { useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { api } from '../../api/index.ts';
import type { MyChange } from '../../api/types.ts';
import { CheckIcon, InfoIcon } from '../../ui/icons.tsx';
@@ -25,13 +25,14 @@ import {
} from '../../ui/tds.tsx';
import { KindChips, SourceChips, StatusChip, statusLabel } from './Badges.tsx';
import {
+ changeLetter,
findReplacingVersion,
formatTime,
- groupByIssue,
+ groupByChange,
isNewStandard,
needsRework,
- RETURNED_TO_SEEN_TEXT,
sameStandardVersions,
+ supersededText,
} from './format.ts';
/**
@@ -42,19 +43,28 @@ const QUESTIONS = {
relation: '이 변경이 내 이슈와 관계가 있나요?',
known: '이 변경을 이미 알고 있었나요?',
};
-const REWORK_NOTICE = '지난번 변경을 반영하셨다면 다시 고쳐야 해요';
+/** 지난번에 "관계있다"로 답한 사람에게 붙이는 안내 (notify.pending.renotify-hint v2) */
+const REWORK_NOTICE =
+ '이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요';
+/**
+ * 대체된 변경에 붙이는 안내 (신지님 시안 10/15).
+ * 대체한 새 버전이 자동으로 닫혀 답할 카드가 없으면 목록과 같은 "이미 확인한 값으로 돌아왔어요"를 써요 (supersededText)
+ */
+const SUPERSEDED_NOTICE =
+ '확인할 변경이 새 버전으로 대체되었어요. 최신 버전의 확인할 변경에 답해 주세요.';
-const STEPS = ['새 변경', '확인 완료', '작업 반영'];
+const STEPS = ['결정 사항 변경', '확인 완료', '이슈 반영'];
const STEP_DESCRIPTIONS = [
'아직 확인하지 않은 변경이에요.',
'담당자가 변경사항을 확인했다고 답한 상태예요.',
- '실제 작업에 반영한 상태예요.',
+ '실제 이슈에 반영한 상태예요.',
];
/** 고르지 않은 큰 버튼 / 고른 큰 버튼 (프로토타입 값 그대로) */
const UNSELECTED_BUTTON: CSSProperties = {
- background: 'rgba(222, 222, 255, 0.30)',
- color: '#FFFFFF',
+ // 고르기 전에는 "현재 결정 사항" 상자와 같은 연한 파랑, 글자는 파랑 (신지님 시안 10/15)
+ background: C.blue50,
+ color: '#1F72EA',
fontSize: 15,
};
const SELECTED_BUTTON: CSSProperties = { fontSize: 15 };
@@ -111,38 +121,28 @@ export function ChangeDetail(props: {
{standard.title}
-
+
- {change.issue.status === 'done' && (
-
- 완료된 이슈
-
- )}
- {isSuperseded && (
-
-
- {replacingVersion === null
- ? RETURNED_TO_SEEN_TEXT
- : `새 버전(v${replacingVersion})으로 대체됐어요`}
+ {/* 바뀐 이유는 이슈 카드 안이 아니라 결정 사항 제목 바로 아래에 붙어요 (신지님 시안 10/15) */}
+
+
+
+ 바뀐 이유
-
-
- {replacingVersion === null
- ? '이 변경에는 답하지 않아도 돼요.'
- : '답하기 전에 결정이 또 바뀌었어요. 이 변경에는 답하지 않아도 되고, 새 버전을 확인해주세요.'}
-
-
{isSuperseded ? (
- replacing && (
- <>
-
- 지금 해야 할 일
-
+ // 대체된 변경 안내는 "지금 해야 할 일" 옆 노란 안내로 보여 줘요 (신지님 시안 10/15)
+ <>
+
+ }
+ >
+ 지금 해야 할 일
+
+ {replacing && (
- >
- )
+ )}
+ >
) : change.answer ? (
<>
내 답변
@@ -204,8 +219,10 @@ export function ChangeDetail(props: {
<>
: undefined}
+ description="내 이슈가 바뀐 결정 사항에 영향을 받는지 답해주세요"
+ right={
+ needsRework(change) ? : undefined
+ }
>
지금 해야 할 일
@@ -245,7 +262,7 @@ function PeerTabs(props: {
onBack: () => void;
onOpen: (pendingCheckId: string) => void;
}) {
- const groups = groupByIssue(props.all);
+ const groups = groupByChange(props.all);
const currentGroup = groups.find((group) =>
group.some((item) => item.pendingCheckId === props.change.pendingCheckId),
);
@@ -288,20 +305,26 @@ function PeerTabs(props: {
padding: '6px 8px 4px 0',
}}
>
- {groups.map((group) => {
+ {groups.map((group, index) => {
const first = group[0]!;
+ const label = `변경 ${changeLetter(index)}`;
const isActive = group === currentGroup;
const target =
group.find((item) => item.status === 'pending') ?? first;
const hasPending = group.some((item) => item.status === 'pending');
return (