From 440e7f83934975bccf8ed6d301d3777fe9098e5f Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:17:04 +0900 Subject: [PATCH 1/9] =?UTF-8?q?docs(wiki):=20=EC=8B=A0=EC=A7=80=EB=8B=98?= =?UTF-8?q?=20=EC=8B=9C=EC=95=88(10/15)=EC=97=90=20=EB=A7=9E=EC=B6=98=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EC=82=AC=ED=95=AD=203=EA=B0=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EA=B8=B0=EB=A1=9D=20=EB=82=A8=EA=B9=80=20?= =?UTF-8?q?(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app.card.three-steps 2→3, app.card.found-in-pr-code-only 1→2, notify.pending.renotify-hint 1→2 Co-Authored-By: Claude Opus 5.5 --- team-wiki/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/team-wiki/CHANGELOG.md b/team-wiki/CHANGELOG.md index 3e4a0e1..b475c24 100644 --- a/team-wiki/CHANGELOG.md +++ b/team-wiki/CHANGELOG.md @@ -169,3 +169,6 @@ 2026-10-15 15:42 | 호윤 | app.notify.title-length 3→4 | 운영체제 알림 창에 뜨는 제목은 공백 포함 30자 이하로 쓴다 → 운영체제 알림 창에 뜨는 제목은 공백 포함 40자 이하로 쓴다 | 웹 앱 알림 시험(v2, v3)을 마치고 원래 값인 40자로 되돌린다 (v1로 되돌림) | AI 대화 2026-10-15 18:25 | 호윤 | copy.term.this-tool 1→2 | 임시 서비스 이름이 정해지기 전까지 문서와 화면에서는 이 도구라고 부르고 예전 가제는 쓰지 않는다 → 문서와 화면에서 서비스 이름은 BANDIT이라고 쓰고 예전 가제 HARIBO는 쓰지 않는다 | 서비스 이름이 HARIBO에서 BANDIT으로 정해졌다 | AI 대화 2026-10-15 18:57 | 민수 | notify.terminal.new-after-answered 1→2 | 같은 세션에서 모두 답한 뒤 새 확인할 변경이 생기면 상세와 질문을 바로 띄우지 않고, 작업을 입력할 때마다 남은 건수와 "결정 사항 확인"을 알리는 한 줄만 보여 준다 → 같은 세션에서 모두 답한 뒤 새 확인할 변경이 생기면 상세와 질문을 바로 띄우지 않고, Claude가 작업을 마칠 때마다 입력 칸 위에 남은 건수와 "결정 사항 확인"을 알리는 한 줄만 보여 준다 | 작업을 입력할 때 띄우면 한 줄이 작업 결과에 밀려 올라가서 못 보기 때문에, 부건님 v2 시안대로 작업을 마친 뒤 입력 칸 바로 위에 띄운다 | AI 대화 +2026-10-15 23:04 | 호윤 | app.card.three-steps 2→3 | 카드의 진행 단계는 새 변경, 확인 완료, 이슈 반영 세 개만 보여 주고 열어봄과 처리 완료는 만들지 않는다 → 카드의 진행 단계는 결정 사항 변경, 확인 완료, 이슈 반영 세 개만 보여 주고 열어봄과 처리 완료는 만들지 않는다 | 신지님 시안(10/15)에서 첫 단계 이름을 결정 사항 변경으로 바꿨다 | AI 대화 +2026-10-15 23:06 | 호윤 | app.card.found-in-pr-code-only 1→2 | 카드의 "PR에서 찾음" 표시는 AI가 찾았고 출처가 코드인 결정 사항에만 붙인다 → 카드의 출처 칩은 AI가 찾았고 출처가 코드인 결정 사항에만 "PR에서 찾은 변경"으로 보여 주고 따로 칩을 더 붙이지 않는다 | 신지님 시안(10/15)에서 PR에서 찾음 칩을 출처 칩 하나로 합쳤다 | AI 대화 +2026-10-15 23:08 | 호윤 | notify.pending.renotify-hint 1→2 | 같은 결정 사항이 다시 바뀌면 지난번에 관계있다로 답한 사람에게만 "지난번 변경(이전 값)을 반영하셨다면 다시 고쳐야 해요." 안내를 붙인다 → 같은 결정 사항이 다시 바뀌면 지난번에 관계있다로 답한 사람에게만 "이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요" 안내를 붙인다 | 신지님 시안(10/15)에서 안내 문구를 이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요로 바꿨다 | AI 대화 From 1115a4817d609812bf5ee49f945ecc5786dda7ff Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:17:04 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix(web):=20=EC=B6=9C=EC=B2=98=20=EC=B9=A9?= =?UTF-8?q?=EC=9D=84=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=95=A9=EC=B9=98?= =?UTF-8?q?=EA=B3=A0=20PR=EC=97=90=EC=84=9C=20=EC=B0=BE=EC=9D=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=97=AC=20?= =?UTF-8?q?=EC=A4=8C=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "…에서 바꿈"을 빼고 [AI 대화] [코드] [Slack] 등 칩 하나만 붙임. AI가 찾았고 출처가 코드면 [PR에서 찾은 변경]. 모두 같은 모양이고 AI 대화에만 반짝 아이콘 (app.card.found-in-pr-code-only v2) Co-Authored-By: Claude Opus 5.5 --- apps/web/src/screens/my-changes/Badges.tsx | 27 +++++++------------ .../web/src/screens/my-changes/format.test.ts | 9 +++---- apps/web/src/screens/my-changes/format.ts | 11 +++++--- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/apps/web/src/screens/my-changes/Badges.tsx b/apps/web/src/screens/my-changes/Badges.tsx index 041c2c1..d1ae87f 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'; @@ -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/format.test.ts b/apps/web/src/screens/my-changes/format.test.ts index 3bc8e48..81da311 100644 --- a/apps/web/src/screens/my-changes/format.test.ts +++ b/apps/web/src/screens/my-changes/format.test.ts @@ -40,13 +40,12 @@ describe('예시 목록', () => { }); describe('카드 표시', () => { - it('PR에서 찾음은 AI가 찾았고 출처가 코드일 때만 (D61)', () => { + it('PR에서 찾은 변경은 AI가 찾았고 출처가 코드일 때만 (D61)', () => { expect(isFoundInPr(byId('inbox-coupon-v2'))).toBe(true); + expect(sourceChipLabel(byId('inbox-coupon-v2'))).toBe('PR에서 찾은 변경'); // T1(AI 대화)은 AI가 먼저 알아챘어도 "AI 대화" 칩만 expect(isFoundInPr(byId('inbox-title-length-v2'))).toBe(false); - expect(sourceChipLabel(byId('inbox-title-length-v2'))).toBe( - 'AI 대화에서 바꿈', - ); + expect(sourceChipLabel(byId('inbox-title-length-v2'))).toBe('AI 대화'); }); it('출처가 위키면 "위키"예요 (app.card.source-chip-only)', () => { @@ -56,7 +55,7 @@ describe('카드 표시', () => { ...wiki, standard: { ...wiki.standard, source: 'wiki' }, }), - ).toBe('위키에서 바꿈'); + ).toBe('위키'); }); it('이전 값이 없으면 새 결정 사항', () => { diff --git a/apps/web/src/screens/my-changes/format.ts b/apps/web/src/screens/my-changes/format.ts index 1aa0a99..bd20606 100644 --- a/apps/web/src/screens/my-changes/format.ts +++ b/apps/web/src/screens/my-changes/format.ts @@ -32,12 +32,17 @@ export function formatTime(iso: string, now = new Date()): string { : `${dateFormat.format(date)} ${time}`; } -/** 출처 칩 글자. 예: "AI 대화에서 바꿈" (app.card.source-chip-only) */ +/** + * 출처 칩 글자. 칩은 하나만 붙여요. 예: "AI 대화", "코드" (app.card.source-chip-only) + * AI가 찾았고 출처가 코드면 "PR에서 찾은 변경" (app.card.found-in-pr-code-only v2) + */ export function sourceChipLabel(change: MyChange): string { - return `${SOURCE_CHIP_LABELS[change.standard.source]}에서 바꿈`; + return isFoundInPr(change) + ? 'PR에서 찾은 변경' + : SOURCE_CHIP_LABELS[change.standard.source]; } -/** "PR에서 찾음"은 AI가 찾았고 출처가 코드일 때만 (app.card.found-in-pr-code-only, D61) */ +/** AI가 찾았고 출처가 코드일 때만 PR에서 찾은 변경이에요 (app.card.found-in-pr-code-only, D61) */ export function isFoundInPr(change: MyChange): boolean { return change.standard.aiDetected && change.standard.source === 'code'; } From ca82a9eb97dd569c5c269323a88eb1d6a9c499f3 Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:17:04 +0900 Subject: [PATCH 3/9] =?UTF-8?q?fix(web):=20=EC=A7=84=ED=96=89=20=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=C2=B7=EB=8B=A4=EC=8B=9C=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=C2=B7=EB=8C=80=EC=B2=B4=EB=90=A8=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=EB=A5=BC=20=EC=8B=A0=EC=A7=80=EB=8B=98=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88(10/15)=EC=97=90=20=EB=A7=9E=EC=B6=A4=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 진행 단계: 결정 사항 변경 → 확인 완료 → 이슈 반영, "이슈에 반영했어요" (app.card.three-steps v3) - 다시 반영 안내: "이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요" (notify.pending.renotify-hint v2) - 대체됨 상세: 회색 상자 대신 "지금 해야 할 일" 옆 노란 안내, 안내 상자는 노란 바탕·검정 글자 - 타입명세서·README 이름도 함께 고침 Co-Authored-By: Claude Opus 5.5 --- apps/web/README.md | 2 +- apps/web/src/api/types.ts | 4 +- .../src/screens/my-changes/ChangeDetail.tsx | 77 ++++++++----------- .../web/src/screens/my-changes/ChangeList.tsx | 3 +- apps/web/src/ui/tds.tsx | 6 +- ...05\353\252\205\354\204\270\354\204\234.md" | 44 +++++------ 6 files changed, 63 insertions(+), 73 deletions(-) diff --git a/apps/web/README.md b/apps/web/README.md index dedc096..18d8e7c 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -95,7 +95,7 @@ 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`(프로토타입 그림)예요. 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/my-changes/ChangeDetail.tsx b/apps/web/src/screens/my-changes/ChangeDetail.tsx index 9ed4eed..9bb46c3 100644 --- a/apps/web/src/screens/my-changes/ChangeDetail.tsx +++ b/apps/web/src/screens/my-changes/ChangeDetail.tsx @@ -4,8 +4,8 @@ * - 답: 관계있다/관계없다 → 알았다/몰랐다(누르면 바로 저장) 또는 관계없는 이유(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'; @@ -30,7 +30,6 @@ import { groupByIssue, isNewStandard, needsRework, - RETURNED_TO_SEEN_TEXT, sameStandardVersions, } from './format.ts'; @@ -42,13 +41,18 @@ const QUESTIONS = { relation: '이 변경이 내 이슈와 관계가 있나요?', known: '이 변경을 이미 알고 있었나요?', }; -const REWORK_NOTICE = '지난번 변경을 반영하셨다면 다시 고쳐야 해요'; +/** 지난번에 "관계있다"로 답한 사람에게 붙이는 안내 (notify.pending.renotify-hint v2) */ +const REWORK_NOTICE = + '이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요'; +/** 대체된 변경에 붙이는 안내 (신지님 시안 10/15) */ +const SUPERSEDED_NOTICE = + '확인할 변경이 새 버전으로 대체되었어요. 최신 버전의 확인할 변경에 답해 주세요.'; -const STEPS = ['새 변경', '확인 완료', '작업 반영']; +const STEPS = ['결정 사항 변경', '확인 완료', '이슈 반영']; const STEP_DESCRIPTIONS = [ '아직 확인하지 않은 변경이에요.', '담당자가 변경사항을 확인했다고 답한 상태예요.', - '실제 작업에 반영한 상태예요.', + '실제 이슈에 반영한 상태예요.', ]; /** 고르지 않은 큰 버튼 / 고른 큰 버튼 (프로토타입 값 그대로) */ @@ -120,30 +124,6 @@ export function ChangeDetail(props: { )} - {isSuperseded && ( -
- - {replacingVersion === null - ? RETURNED_TO_SEEN_TEXT - : `새 버전(v${replacingVersion})으로 대체됐어요`} - -
- - {replacingVersion === null - ? '이 변경에는 답하지 않아도 돼요.' - : '답하기 전에 결정이 또 바뀌었어요. 이 변경에는 답하지 않아도 되고, 새 버전을 확인해주세요.'} - -
-
- )} -
@@ -169,11 +149,12 @@ export function ChangeDetail(props: {
{isSuperseded ? ( - replacing && ( - <> - - 지금 해야 할 일 - + // 대체된 변경 안내는 "지금 해야 할 일" 옆 노란 안내로 보여 줘요 (신지님 시안 10/15) + <> + }> + 지금 해야 할 일 + + {replacing && (
- - ) + )} + ) : change.answer ? ( <> 내 답변 @@ -205,7 +186,9 @@ export function ChangeDetail(props: { : undefined} + right={ + needsRework(change) ? : undefined + } > 지금 해야 할 일 @@ -505,7 +488,7 @@ function IssueStateChips(props: { change: MyChange }) { {change.answer.known ? '알았다' : '몰랐다'} )} {change.reflected ? ( - 작업 반영 + 이슈 반영 ) : ( 확인 완료 )} @@ -539,7 +522,8 @@ function ValueBox(props: { label: string; value: string; isAfter?: boolean }) { ); } -function ReworkNotice() { +/** "지금 해야 할 일" 옆 노란 안내 (신지님 시안 10/15: 노란 바탕, 검정 글자) */ +function Notice(props: { text: string }) { return (
- {REWORK_NOTICE} + {props.text}
); @@ -787,7 +772,7 @@ function AnsweredBox(props: { ); await props.onSaved(); props.onToast( - `내 작업 상태를 ${isReflected ? '확인 완료' : '작업 반영'}로 바꿨어요.`, + `상태를 바꿨어요 · ${isReflected ? '확인 완료' : '이슈 반영'}`, ); } catch (caught: unknown) { setError(caught instanceof Error ? caught.message : String(caught)); @@ -841,7 +826,7 @@ function AnsweredBox(props: { - 작업 반영했어요 + 이슈에 반영했어요 )} diff --git a/apps/web/src/screens/my-changes/ChangeList.tsx b/apps/web/src/screens/my-changes/ChangeList.tsx index 28a9518..3a883f9 100644 --- a/apps/web/src/screens/my-changes/ChangeList.tsx +++ b/apps/web/src/screens/my-changes/ChangeList.tsx @@ -236,7 +236,8 @@ function ChangeRow(props: { {isPending && needsRework(change) && ( - 지난번 변경을 반영하셨다면 다시 고쳐야 해요 + 이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 + 주세요 )} {isSuperseded && ( diff --git a/apps/web/src/ui/tds.tsx b/apps/web/src/ui/tds.tsx index 6fe8762..9fa45e0 100644 --- a/apps/web/src/ui/tds.tsx +++ b/apps/web/src/ui/tds.tsx @@ -681,7 +681,11 @@ export function SectionTitle(props: { }} > {texts} - {props.right &&
{props.right}
} + {props.right && ( +
+ {props.right} +
+ )} ); } diff --git "a/docs/\355\203\200\354\236\205\353\252\205\354\204\270\354\204\234.md" "b/docs/\355\203\200\354\236\205\353\252\205\354\204\270\354\204\234.md" index d33c7f4..e764f63 100644 --- "a/docs/\355\203\200\354\236\205\353\252\205\354\204\270\354\204\234.md" +++ "b/docs/\355\203\200\354\236\205\353\252\205\354\204\270\354\204\234.md" @@ -103,23 +103,23 @@ ### `StandardVersion` — 기준 버전 (바뀔 때마다 한 줄) -| 칸 | 타입 | 비어 있나 | DB 칸 | 뜻 | -| ------------------- | ----------------------- | --------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `standardId` | `StandardId` | 아니요 | `standard_id` | | -| `version` | `number` | 아니요 | `version` | 1부터 하나씩 올라가요 | -| `title` | `string` | 아니요 | `title` | 버전마다 남겨요 (SIL-57). 40자를 넘으면 경고만 해요 (2절) | -| `value` | `string` | 아니요 | `value` | 기준 내용 한 문장 (화면 기준 120자, 2절) | -| `reason` | `string` | 아니요 | `reason` | 없으면 저장을 거절해요 | -| `alwaysOn` | `boolean` | 아니요 | `always_on` | **그 버전의** 항상 적용. 되돌리면 그 버전의 값으로 돌아가요 (2026-10-10 추가) | -| `evidence` | `string \| null` | 예 | `evidence` | 근거. 선택이에요(PR에서 찾은 변경은 필수). 있으면 저장하고 알림 카드에는 보여 주지 않아요 | -| `scope` | `StandardScope \| null` | 예 | `scope` | 적용 범위. 저장만 해요 | -| `changedBy` | `PersonRef \| null` | 예 | `changed_by` | 누가 바꿨나. MCP로 저장하면 그 세션의 사용자 이름이 들어가요 | -| `changedByActor` | `Actor` | 아니요 | `changed_by_actor` | AI가 저장했으면 `'ai'` | -| `aiDetected` | `boolean` | 아니요 | `ai_detected` | AI가 먼저 알아챈 변경이면 `true`: PR에서 찾음(출처 `code`) 또는 작업 중에 먼저 물은 T1(출처 `ai_chat`). 화면의 "PR에서 찾음"은 출처가 `code`일 때만 (D61) | -| `isRevert` | `boolean` | 아니요 | `is_revert` | 앞선 버전 값과 같아졌으면 `true` | -| `revertedToVersion` | `number \| null` | 예 | `reverted_to_version` | 되돌림이면 어느 버전으로 돌아갔는지 | -| `source` | `StandardSource` | 아니요 | `source` | 출처 | -| `createdAt` | `IsoDateTime` | 아니요 | `created_at` | 저장한 시각 | +| 칸 | 타입 | 비어 있나 | DB 칸 | 뜻 | +| ------------------- | ----------------------- | --------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `standardId` | `StandardId` | 아니요 | `standard_id` | | +| `version` | `number` | 아니요 | `version` | 1부터 하나씩 올라가요 | +| `title` | `string` | 아니요 | `title` | 버전마다 남겨요 (SIL-57). 40자를 넘으면 경고만 해요 (2절) | +| `value` | `string` | 아니요 | `value` | 기준 내용 한 문장 (화면 기준 120자, 2절) | +| `reason` | `string` | 아니요 | `reason` | 없으면 저장을 거절해요 | +| `alwaysOn` | `boolean` | 아니요 | `always_on` | **그 버전의** 항상 적용. 되돌리면 그 버전의 값으로 돌아가요 (2026-10-10 추가) | +| `evidence` | `string \| null` | 예 | `evidence` | 근거. 선택이에요(PR에서 찾은 변경은 필수). 있으면 저장하고 알림 카드에는 보여 주지 않아요 | +| `scope` | `StandardScope \| null` | 예 | `scope` | 적용 범위. 저장만 해요 | +| `changedBy` | `PersonRef \| null` | 예 | `changed_by` | 누가 바꿨나. MCP로 저장하면 그 세션의 사용자 이름이 들어가요 | +| `changedByActor` | `Actor` | 아니요 | `changed_by_actor` | AI가 저장했으면 `'ai'` | +| `aiDetected` | `boolean` | 아니요 | `ai_detected` | AI가 먼저 알아챈 변경이면 `true`: PR에서 찾음(출처 `code`) 또는 작업 중에 먼저 물은 T1(출처 `ai_chat`). 화면의 출처 칩 "PR에서 찾은 변경"은 출처가 `code`일 때만 (D61) | +| `isRevert` | `boolean` | 아니요 | `is_revert` | 앞선 버전 값과 같아졌으면 `true` | +| `revertedToVersion` | `number \| null` | 예 | `reverted_to_version` | 되돌림이면 어느 버전으로 돌아갔는지 | +| `source` | `StandardSource` | 아니요 | `source` | 출처 | +| `createdAt` | `IsoDateTime` | 아니요 | `created_at` | 저장한 시각 | - `StandardScope` = `{ code: string[]; api: string[]; screen: string[]; doc: string[] }`. 칸이 빠진 채로 저장하면 빈 목록으로 채워요. - `StandardSource` = `'code' | 'ai_chat' | 'figma' | 'meeting' | 'slack' | 'wiki'` @@ -492,8 +492,8 @@ try { | 이전 값 → 지금 값 | 이전 값(아래 규칙), `StandardVersion.value` | 이전 값이 `null`이면 "새 결정 사항" | | 이유 | `StandardVersion.reason` | 그대로 | | 누가 바꿨나 | `StandardVersion.changedBy` | 이름만 | -| AI가 찾았나 | `StandardVersion.aiDetected` | `true`이고 출처가 `code`일 때만 "PR에서 찾음" (D61) | -| 출처 | `StandardVersion.source` | `SOURCE_LABELS`로 "AI 대화에서 바꿈" / "코드에서 바꿈" | +| AI가 찾았나 | `StandardVersion.aiDetected` | `true`이고 출처가 `code`일 때만 출처 칩이 "PR에서 찾은 변경" (D61) | +| 출처 | `StandardVersion.source` | `SOURCE_CHIP_LABELS`로 "AI 대화" / "코드" 등, 칩은 하나만 | | 언제 | `StandardVersion.createdAt` | 오늘이면 "오후 2:13", 아니면 "10월 13일 오후 2:13" (보는 사람의 시간대) | | 내 이슈 | `Issue.key`, `Issue.title` | 제목이 길면 끝을 줄여요(…) | | 이슈 상태 | `Issue.status` | `'done'`일 때만 "완료" | @@ -534,8 +534,8 @@ try { | `pendingCheckId` | `string` | `PendingCheck.id` | | `status` | `'pending' \| 'answered' \| 'superseded'` | 자동 닫힘은 목록에 없어요 (notify.pending.hide-auto-closed) | | `createdAt` | `string` | 진행 단계 "새 변경" | -| `reflected` | `boolean` | 작업 반영함 (notify.check.reflected) | -| `reflectedAt` | `string \| null` | 진행 단계 "작업 반영" | +| `reflected` | `boolean` | 이슈 반영함 (notify.check.reflected) | +| `reflectedAt` | `string \| null` | 진행 단계 "이슈 반영" | | `standard` | `MyChangeStandard` | 확인할 그 버전: `id`, `title`, `version`, `value`, `previousVersion`, `previousValue`(11-4 "이전 값 고르기"), `reason`, `changedBy`, `changedAt`, `source`, `aiDetected`, `isRevert` | | `issue` | `MyChangeIssue` | 내 이슈: `key`, `title`, `status` | | `answer` | `MyChangeAnswer` 또는 `null` | 내 답: `affected`, `known`, `unaffectedReason`, `channel`, `answeredAt`. 대기·대체됨이면 `null` | @@ -551,7 +551,7 @@ try { | `watchMyChanges(me, onChange, onError)` | `watchPendingChecks`(`@sil/core/data`, Realtime) 구독. 돌려준 함수로 끊어요 (app.notify.realtime) | - **내 이름(`me`)**: 주소에 `?me=<이름>`을 붙여 열면 브라우저에 저장하고 주소에서 지워요. 다음부터는 저장한 이름을 써요. 이름을 고르는 화면은 만들지 않아요 (10/15 호윤, `apps/web/src/me.ts`). 예시 데이터로 열면 이름이 없어도 예시를 받는 사람(`INBOX_RECIPIENT`)으로 열려요. -- **예시 데이터**: `@sil/core`의 `inboxIssues`, `inboxVersions`, `inboxPendingChecks`, `inboxAnswers`예요. 화면의 상태(사람이 바꾼 결정 사항, PR에서 찾음, 새 결정 사항, 대체됨, 되돌림, 답함)를 모두 담았어요. +- **예시 데이터**: `@sil/core`의 `inboxIssues`, `inboxVersions`, `inboxPendingChecks`, `inboxAnswers`예요. 화면의 상태(사람이 바꾼 결정 사항, PR에서 찾은 변경, 새 결정 사항, 대체됨, 되돌림, 답함)를 모두 담았어요. - **대체한 버전 번호**: 서버가 알려 주지 않아서 같은 목록에서 같은 결정 사항·같은 이슈의 더 높은 버전 중 가장 가까운 것을 찾아요. 못 찾으면 대체한 버전이 자동으로 닫힌 것이라(내가 이미 확인한 값으로 되돌려짐) "새 버전으로 대체됐다가, 내가 이미 확인한 값으로 돌아왔어요"라고 써요. - **OS 알림(A1)**: 창이 열려 있는 동안 구독으로 목록을 다시 읽고, 처음 불러온 뒤 새로 생긴 대기 변경만 `showChangeNotification`으로 띄워요. 제목은 40자 이하로 줄여요 (app.notify.title-length). From 7bc402330b2c2ef6bbf946bd0f634910728d27db Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:21:46 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix(web):=20=EB=82=98=EC=97=90=EA=B2=8C=20?= =?UTF-8?q?=EC=98=A8=20=EB=B3=80=EA=B2=BD=20=EB=AA=A9=EB=A1=9D=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=C2=B7=EB=AA=A8=EC=96=91=EC=9D=84=20=EC=8B=A0=EC=A7=80?= =?UTF-8?q?=EB=8B=98=20=EC=8B=9C=EC=95=88(10/15)=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 탭 "이슈별", 설명 "답한 변경은 7일 뒤에 목록에서 내려가요", 빈 화면 "지금 … 없어요 / 알려 드릴게요", 아래 줄 가운뎃점 빼기, 완료된 이슈는 [완료] 칩, 이전 값만 더 연하게, 되돌림 줄의 답한 이력 숨김 Co-Authored-By: Claude Opus 5.5 --- apps/web/public/notify-guide.svg | 125 ------------------ apps/web/src/screens/MyChangesScreen.tsx | 5 +- .../web/src/screens/my-changes/ChangeList.tsx | 63 +++++---- apps/web/src/ui/tds.tsx | 2 +- 4 files changed, 34 insertions(+), 161 deletions(-) delete mode 100644 apps/web/public/notify-guide.svg 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/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/ChangeList.tsx b/apps/web/src/screens/my-changes/ChangeList.tsx index 3a883f9..621a726 100644 --- a/apps/web/src/screens/my-changes/ChangeList.tsx +++ b/apps/web/src/screens/my-changes/ChangeList.tsx @@ -1,11 +1,11 @@ /** * A2 목록이에요. "확인할 변경"(대기)과 "확인한 변경"(답함, 대체됨) 두 상자로 나눠요. - * 목록은 한 번 불러서 화면이 상태로 나눠요 (notify.list.one-call). "이슈 별로 보기"는 이슈 번호로 묶어요. + * 목록은 한 번 불러서 화면이 상태로 나눠요 (notify.list.one-call). "이슈별"은 이슈 번호로 묶어요. * 상자·줄·글자 값은 프로토타입(SIL-69)과 같아요. */ import type { CSSProperties, ReactNode } from 'react'; import type { MyChange } from '../../api/types.ts'; -import { C, ListRow, Result, Text } from '../../ui/tds.tsx'; +import { C, Chip, ListRow, Result, Text } from '../../ui/tds.tsx'; import { KindChips, SourceChips, StatusCell } from './Badges.tsx'; import { findReplacingVersion, @@ -50,7 +50,7 @@ export function ChangeList(props: {
)} @@ -130,20 +130,20 @@ function EmptyBoxes() { {emptyBox( C.blue100, OPEN_TITLE, - `아직 ${OPEN_TITLE}이 없어요`, - '새로운 변경이 감지되면 이곳에서 알려드릴게요', + `지금 ${OPEN_TITLE}이 없어요`, + '새로운 변경이 감지되면 이곳에서 알려 드릴게요', )} {emptyBox( C.grey200, CLOSED_TITLE, - `아직 ${CLOSED_TITLE}이 없어요`, + `지금 ${CLOSED_TITLE}이 없어요`, '변경에 답하면 이곳에 모여요', )} ); } -/** 이슈 별로 보기: 이슈마다 상자 하나. 답할 변경이 있는 이슈가 먼저, 상자 안은 높은 버전부터 */ +/** 이슈별: 이슈마다 상자 하나. 답할 변경이 있는 이슈가 먼저, 상자 안은 높은 버전부터 */ function IssueGroups(props: { changes: MyChange[]; onOpen: (pendingCheckId: string) => void; @@ -297,37 +297,37 @@ function TitleLine(props: { change: MyChange }) { > + ); } -/** 이전 값 → 지금 값. 되돌림이면 내가 답했던 버전을 함께 적어요 */ +/** + * 이전 값 → 지금 값. 이전 값만 더 연한 회색이에요 (신지님 시안 10/15). + * 되돌림 줄의 "내가 v2에 답한 이력이 있어요"는 시안에서 숨겼어요. + */ function ValueLine(props: { change: MyChange }) { - const { standard, previousAnswer } = props.change; - const values = ( + const { standard } = props.change; + return ( - {standard.previousValue === null - ? '새 결정 사항' - : `${standard.previousValue} (v${standard.previousVersion})`}{' '} + + {standard.previousValue === null + ? '새 결정 사항' + : `${standard.previousValue} (v${standard.previousVersion})`} + {' '} → {standard.value} (v{standard.version}) ); - if (!standard.isRevert || !previousAnswer) return values; +} + +/** 완료된 이슈면 제목 옆 칩들 맨 끝에 [완료] (신지님 시안 10/15) */ +function DoneIssueChip(props: { change: MyChange }) { + if (props.change.issue.status !== 'done') return null; return ( -
- {values} - - · 내가 v{previousAnswer.version}에 답한 이력이 있어요 - -
+ + 완료 + ); } @@ -349,7 +349,7 @@ function ReworkNote(props: { children: ReactNode }) { ); } -/** #5 · 이슈 제목 (완료된 이슈) · 바꾼 사람 · 시각 */ +/** #5 이슈 제목 바꾼 사람 시각. 가운뎃점 없이 띄어 써요 (신지님 시안 10/15) */ function MetaLine(props: { change: MyChange }) { const { issue, standard } = props.change; return ( @@ -357,7 +357,7 @@ function MetaLine(props: { change: MyChange }) { style={{ display: 'flex', alignItems: 'center', - gap: 4, + gap: 8, minWidth: 0, color: C.grey500, }} @@ -367,13 +367,12 @@ function MetaLine(props: { change: MyChange }) {
- · {issue.title} - {issue.status === 'done' ? ' (완료된 이슈)' : ''} + {issue.title}
- · {standard.changedBy ?? '모름'} · {formatTime(standard.changedAt)} + {standard.changedBy ?? '모름'} {formatTime(standard.changedAt)}
diff --git a/apps/web/src/ui/tds.tsx b/apps/web/src/ui/tds.tsx index 9fa45e0..f869cc7 100644 --- a/apps/web/src/ui/tds.tsx +++ b/apps/web/src/ui/tds.tsx @@ -304,7 +304,7 @@ export function TextButton(props: { ); } -/** 두세 개 중 하나를 고르는 칸 (전체 / 이슈 별로 보기) */ +/** 두세 개 중 하나를 고르는 칸 (전체 / 이슈별) */ export function Segmented(props: { label: string; value: T; From 90c15f7f042cd36e1d85cfda0ef8e5a4914300b5 Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:21:46 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix(web):=20=EB=B3=80=EA=B2=BD=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=AC=B8=EA=B5=AC=C2=B7=EB=AA=A8=EC=96=91=EC=9D=84?= =?UTF-8?q?=20=EC=8B=A0=EC=A7=80=EB=8B=98=20=EC=8B=9C=EC=95=88(10/15)?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 값 상자 "이전 결정 사항 / 현재 결정 사항", 바뀐 이유를 제목 아래로, [되돌림]은 현재 결정 사항 상자 옆, [완료]는 이슈 카드, 고르기 전 답 버튼은 연한 파랑, 다음 변경 화살표 작게, 알림 "답변을 완료했어요.", 지금 보는 이슈 칩에는 제목을 띄우지 않음, 지금 해야 할 일 설명 "내 이슈가 바뀐 결정 사항에…" Co-Authored-By: Claude Opus 5.5 --- apps/web/src/screens/my-changes/Badges.tsx | 6 +- .../src/screens/my-changes/ChangeDetail.tsx | 135 +++++++++++++----- 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/apps/web/src/screens/my-changes/Badges.tsx b/apps/web/src/screens/my-changes/Badges.tsx index d1ae87f..a0fd74f 100644 --- a/apps/web/src/screens/my-changes/Badges.tsx +++ b/apps/web/src/screens/my-changes/Badges.tsx @@ -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 && ( 되돌림 diff --git a/apps/web/src/screens/my-changes/ChangeDetail.tsx b/apps/web/src/screens/my-changes/ChangeDetail.tsx index 9bb46c3..468454b 100644 --- a/apps/web/src/screens/my-changes/ChangeDetail.tsx +++ b/apps/web/src/screens/my-changes/ChangeDetail.tsx @@ -8,7 +8,7 @@ * - "이슈에 반영했어요"는 "관계있다"로 답한 카드에만, 답한 뒤에만 켜요. 풀어도 이슈를 다시 열지 않아요 (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'; @@ -57,8 +57,9 @@ 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 }; @@ -115,13 +116,27 @@ export function ChangeDetail(props: { {standard.title} - + - {change.issue.status === 'done' && ( - - 완료된 이슈 - - )} + + + {/* 바뀐 이유는 이슈 카드 안이 아니라 결정 사항 제목 바로 아래에 붙어요 (신지님 시안 10/15) */} +
+
+ + 바뀐 이유 + +
+ + {standard.reason} +
@@ -136,13 +151,20 @@ export function ChangeDetail(props: { ) : ( <> + 되돌림 + + ) : undefined + } /> )} @@ -185,7 +207,7 @@ export function ChangeDetail(props: { <> : undefined } @@ -284,7 +306,12 @@ function PeerTabs(props: { role="tab" aria-selected={isActive} aria-label={`이슈 ${first.issue.key} ${first.standard.title} (${statusLabel(first)})`} - title={`${first.standard.title} · ${statusLabel(first)}`} + // 지금 보고 있는 이슈 칩에는 제목을 띄우지 않아요 (신지님 시안 10/15) + title={ + isActive + ? undefined + : `${first.standard.title} · ${statusLabel(first)}` + } onClick={() => !isActive && props.onOpen(target.pendingCheckId)} style={{ position: 'relative', @@ -455,30 +482,24 @@ function IssueCard(props: { change: MyChange }) { {change.standard.changedBy ?? '모름'} ·{' '} {formatTime(change.standard.changedAt)} -
-
- - 바뀐 이유 - -
- - {change.standard.reason} - -
); } +/** 이슈 카드 오른쪽 상태 칩. 완료된 이슈면 [완료]를 하나 더 붙여요 (신지님 시안 10/15) */ function IssueStateChips(props: { change: MyChange }) { + const { change } = props; + return ( + <> + + {change.issue.status === 'done' && 완료} + + ); +} + +function AnswerStateChips(props: { change: MyChange }) { const { change } = props; if (change.status === 'pending') return 대기; if (change.status === 'superseded') return 대체됨; @@ -496,7 +517,13 @@ function IssueStateChips(props: { change: MyChange }) { ); } -function ValueBox(props: { label: string; value: string; isAfter?: boolean }) { +function ValueBox(props: { + label: string; + value: string; + isAfter?: boolean; + /** 이름 옆에 붙는 칩 (되돌림) */ + tag?: ReactNode; +}) { return (
-
+
{props.label} + {props.tag}
{props.value} @@ -623,7 +653,7 @@ function AnswerForm(props: { elapsedSeconds: Math.round((Date.now() - openedAt.current) / 1000), }); await props.onSaved(); - props.onToast('답했어요. 확인할 변경 상태가 답함으로 바뀌었어요.'); + props.onToast('답변을 완료했어요.'); } catch (error: unknown) { if (isSilError(error) && error.code === 'PENDING_CLOSED') { props.onClosedElsewhere(); @@ -870,11 +900,44 @@ function AnsweredBox(props: { {CHANNEL_LABELS[answer.channel]}에서 답함 {props.onNext && ( - - + // 화살표는 작게(12px), 글자와 같은 얇은 굵기로 (신지님 시안 10/15) + )}
From 4eb3bae39879aa3a482b137da9a7bbfd6416421d Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:21:46 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix(web):=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=A9=EB=B2=95=20=EC=95=88=EB=82=B4=EB=A5=BC=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=BC=203=EC=9E=A5=EA=B3=BC=20=EC=A7=84=EC=A7=9C?= =?UTF-8?q?=20=EA=B8=80=EC=9E=90=EB=A1=9C=20=EB=B0=94=EA=BF=88=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신지님 시안(10/15)의 notify-step-1~3.svg를 쓰고, 그림 아래 단계 글자를 텍스트로 보여 줌 ("…해요"체). 그림 한 장을 잘라 쓰던 notify-guide.svg는 지움 Co-Authored-By: Claude Opus 5.5 --- apps/web/README.md | 2 +- apps/web/public/notify-step-1.svg | 39 ++++++++ apps/web/public/notify-step-2.svg | 43 +++++++++ apps/web/public/notify-step-3.svg | 45 +++++++++ .../screens/my-changes/NotificationBanner.tsx | 91 ++++++++++++------- apps/web/src/styles.css | 5 +- 6 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 apps/web/public/notify-step-1.svg create mode 100644 apps/web/public/notify-step-2.svg create mode 100644 apps/web/public/notify-step-3.svg diff --git a/apps/web/README.md b/apps/web/README.md index 18d8e7c..179e6eb 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -98,7 +98,7 @@ await showChangeNotification({ | 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-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/screens/my-changes/NotificationBanner.tsx b/apps/web/src/screens/my-changes/NotificationBanner.tsx index 6a2bc14..df32611 100644 --- a/apps/web/src/screens/my-changes/NotificationBanner.tsx +++ b/apps/web/src/screens/my-changes/NotificationBanner.tsx @@ -17,16 +17,29 @@ import { C, Text } from '../../ui/tds.tsx'; /** 펼침 애니메이션 시간 (styles.css .notify-panel과 같아요) */ const PANEL_MS = 420; -/** 안내 그림(public/notify-guide.svg) 한 장에서 세 칸을 잘라 보여 줘요 */ -const GUIDE_COLUMNS = [ - { x: 20, width: 240 }, - { x: 324, width: 240 }, - { x: 630, width: 220 }, -]; +/** + * 설정 방법 3단계: 그림(public/notify-step-1~3.svg, 240 × 100)과 그 아래 진짜 글자예요 (신지님 시안 10/15). + * 글자 크기는 안내 머리글과 같아요: 제목 t6 굵게, 설명 st12 + */ const GUIDE_STEPS = [ - '사이트 정보 아이콘을 클릭. 브라우저에서 주소창 왼쪽의 자물쇠 또는 사이트 정보 아이콘을 클릭합니다', - '알림 권한을 허용으로 변경하세요. 사이트 설정 메뉴에서 알림 항목을 찾아 허용으로 변경합니다', - '이제 알림을 받을 수 있어요. 설정 변경 후 이 화면으로 돌아와 권한을 다시 확인해 주세요', + { + image: 'notify-step-1.svg', + title: '사이트 정보 아이콘을 클릭', + lines: [ + '브라우저에서 주소창 왼쪽의', + '자물쇠 또는 사이트 정보 아이콘을 클릭해요', + ], + }, + { + image: 'notify-step-2.svg', + title: '알림 권한을 허용으로 변경하세요', + lines: ['사이트 설정 메뉴에서', '알림 항목을 찾아 허용으로 변경해요'], + }, + { + image: 'notify-step-3.svg', + title: '이제 알림을 받을 수 있어요', + lines: ['설정 변경 후 이 화면으로 돌아와', '권한을 다시 확인해 주세요'], + }, ]; const DESCRIPTIONS: Record< @@ -212,39 +225,49 @@ function Banner(props: { >
- {GUIDE_COLUMNS.map((column) => ( -
+ {GUIDE_STEPS.map((step) => ( +
+
- + + {step.title} + + + {step.lines.map((line) => ( + + {line} + + ))} +
))}
-
    - {GUIDE_STEPS.map((step) => ( -
  1. {step}
  2. - ))} -
)} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 8c2fa96..d5a75e0 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -238,7 +238,10 @@ button { flex: 1 1 0; min-width: 260px; display: flex; - justify-content: center; + flex-direction: column; + align-items: center; + padding: 22px 16px 28px; + box-sizing: border-box; background: #fafafa; border-radius: 15px; } From b2ee3fd94886e3290c046e1ab5f3b7298c9345c2 Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:26:01 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix(web):=20=EB=8B=B5=ED=95=A0=20=EC=83=88?= =?UTF-8?q?=20=EB=B2=84=EC=A0=84=20=EC=B9=B4=EB=93=9C=EA=B0=80=20=EC=97=86?= =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=EB=8C=80=EC=B2=B4=EB=90=A8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EB=8F=84=20=EC=9D=B4=EB=AF=B8=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EB=8F=8C=EC=95=84?= =?UTF-8?q?=EC=99=94=EB=8B=A4=EA=B3=A0=20=EC=95=8C=EB=A0=A4=20=EC=A4=8C=20?= =?UTF-8?q?(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새 버전 카드가 있으면 시안 문구 "최신 버전의 확인할 변경에 답해 주세요", 자동으로 닫혀 없으면 목록과 같은 "새 버전으로 대체됐다가, 내가 이미 확인한 값으로 돌아왔어요. 답하지 않아도 돼요" Co-Authored-By: Claude Opus 5.5 --- apps/web/src/screens/my-changes/ChangeDetail.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/web/src/screens/my-changes/ChangeDetail.tsx b/apps/web/src/screens/my-changes/ChangeDetail.tsx index 468454b..b0edda2 100644 --- a/apps/web/src/screens/my-changes/ChangeDetail.tsx +++ b/apps/web/src/screens/my-changes/ChangeDetail.tsx @@ -31,6 +31,7 @@ import { isNewStandard, needsRework, sameStandardVersions, + supersededText, } from './format.ts'; /** @@ -44,7 +45,10 @@ const QUESTIONS = { /** 지난번에 "관계있다"로 답한 사람에게 붙이는 안내 (notify.pending.renotify-hint v2) */ const REWORK_NOTICE = '이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요'; -/** 대체된 변경에 붙이는 안내 (신지님 시안 10/15) */ +/** + * 대체된 변경에 붙이는 안내 (신지님 시안 10/15). + * 대체한 새 버전이 자동으로 닫혀 답할 카드가 없으면 목록과 같은 "이미 확인한 값으로 돌아왔어요"를 써요 (supersededText) + */ const SUPERSEDED_NOTICE = '확인할 변경이 새 버전으로 대체되었어요. 최신 버전의 확인할 변경에 답해 주세요.'; @@ -173,7 +177,14 @@ export function ChangeDetail(props: { {isSuperseded ? ( // 대체된 변경 안내는 "지금 해야 할 일" 옆 노란 안내로 보여 줘요 (신지님 시안 10/15) <> - }> + + } + > 지금 해야 할 일 {replacing && ( From 300941e062ca7f11643494966c1d81e511bfe3a1 Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:41:02 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs(wiki):=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=9C=84=20=EC=B9=A9=EC=9D=84=20=EB=B3=80=EA=B2=BD=20A,=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20B=EC=B2=98=EB=9F=BC=20=EB=B6=80=EB=A5=B4?= =?UTF-8?q?=EB=8A=94=20=EA=B2=B0=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app.detail.change-letter new→1 (T1 직접 요청, AI 대화) Co-Authored-By: Claude Opus 5.5 --- team-wiki/CHANGELOG.md | 1 + team-wiki/INDEX.md | 2 +- "team-wiki/\354\225\261-\355\231\224\353\251\264.md" | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/team-wiki/CHANGELOG.md b/team-wiki/CHANGELOG.md index b475c24..bc758eb 100644 --- a/team-wiki/CHANGELOG.md +++ b/team-wiki/CHANGELOG.md @@ -172,3 +172,4 @@ 2026-10-15 23:04 | 호윤 | app.card.three-steps 2→3 | 카드의 진행 단계는 새 변경, 확인 완료, 이슈 반영 세 개만 보여 주고 열어봄과 처리 완료는 만들지 않는다 → 카드의 진행 단계는 결정 사항 변경, 확인 완료, 이슈 반영 세 개만 보여 주고 열어봄과 처리 완료는 만들지 않는다 | 신지님 시안(10/15)에서 첫 단계 이름을 결정 사항 변경으로 바꿨다 | AI 대화 2026-10-15 23:06 | 호윤 | app.card.found-in-pr-code-only 1→2 | 카드의 "PR에서 찾음" 표시는 AI가 찾았고 출처가 코드인 결정 사항에만 붙인다 → 카드의 출처 칩은 AI가 찾았고 출처가 코드인 결정 사항에만 "PR에서 찾은 변경"으로 보여 주고 따로 칩을 더 붙이지 않는다 | 신지님 시안(10/15)에서 PR에서 찾음 칩을 출처 칩 하나로 합쳤다 | AI 대화 2026-10-15 23:08 | 호윤 | notify.pending.renotify-hint 1→2 | 같은 결정 사항이 다시 바뀌면 지난번에 관계있다로 답한 사람에게만 "지난번 변경(이전 값)을 반영하셨다면 다시 고쳐야 해요." 안내를 붙인다 → 같은 결정 사항이 다시 바뀌면 지난번에 관계있다로 답한 사람에게만 "이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요" 안내를 붙인다 | 신지님 시안(10/15)에서 안내 문구를 이전 결정 사항으로 작업 중이었다면 최신 결정 사항을 반영해 주세요로 바꿨다 | AI 대화 +2026-10-15 23:39 | 호윤 | app.detail.change-letter new→1 | (없음) → 변경 상세 위 칩은 결정 사항과 내 이슈 한 쌍마다 하나씩 두고 받은 순서대로 변경 A, 변경 B처럼 부른다 | 칩 하나가 결정 사항 하나를 가리키는데 결정 사항 ID는 칩에 넣기에 너무 길고 결정 사항에 정해진 번호가 없다 | AI 대화 diff --git a/team-wiki/INDEX.md b/team-wiki/INDEX.md index 4cd8b48..1169c03 100644 --- a/team-wiki/INDEX.md +++ b/team-wiki/INDEX.md @@ -24,7 +24,7 @@ | wiki | 기능-기준모으기.md | 기준을 저장·조회·되돌리는 규칙과 MCP 도구 | 13 | | detect | 기능-바뀐기준찾기.md | 반영된 PR을 보고 바뀐 기준을 찾는 방법과 안전 규칙 | 16 | | notify | 기능-알리기.md | 이슈 연결, 영향받는 담당자 찾기, 터미널 알림, 응답, 예외 처리 | 30 | -| app | 앱-화면.md | 데스크톱 앱 화면, 앱 알림, 설치 | 11 | +| app | 앱-화면.md | 데스크톱 앱 화면, 앱 알림, 설치 | 12 | | data | 데이터-구조.md | ID 형식, 기록 형식, 상태 값 | 9 | | copy | 문구.md | 알림과 AI 설명의 문장 규칙, 함께 쓰는 말 | 8 | | work | 일하는-방식.md | 결과물에 영향을 주는 팀 규칙 (위키 기록, 브랜치, 리뷰, 배포) | 10 | diff --git "a/team-wiki/\354\225\261-\355\231\224\353\251\264.md" "b/team-wiki/\354\225\261-\355\231\224\353\251\264.md" index fcfa6ad..41d185a 100644 --- "a/team-wiki/\354\225\261-\355\231\224\353\251\264.md" +++ "b/team-wiki/\354\225\261-\355\231\224\353\251\264.md" @@ -146,3 +146,13 @@ - 항상 적용: 아니오 - 상태: 사용 - 버전: 1 + +### app.detail.change-letter +- 제목: 상세 위 칩은 변경 A, 변경 B처럼 부른다 +- 값: 변경 상세 위 칩은 결정 사항과 내 이슈 한 쌍마다 하나씩 두고 받은 순서대로 변경 A, 변경 B처럼 부른다 +- 이유: 칩 하나가 결정 사항 하나를 가리키는데 결정 사항 ID는 칩에 넣기에 너무 길고 결정 사항에 정해진 번호가 없다 +- 적용 범위: 화면: 나에게 온 변경 / 코드: apps/web/src/screens/** +- 근거: AI 대화 (10/15 호윤님 결정) +- 항상 적용: 아니오 +- 상태: 사용 +- 버전: 1 From 16cc743bc69cf86f6d05f214fd60e1eec82ebba0 Mon Sep 17 00:00:00 2001 From: space-angel Date: Thu, 15 Oct 2026 23:41:02 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix(web):=20=EC=83=81=EC=84=B8=20=EC=9C=84?= =?UTF-8?q?=20=EC=B9=A9=EC=9D=84=20=EA=B2=B0=EC=A0=95=20=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=B4=EC=8A=88=20=ED=95=9C=20=EC=8C=8D=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=ED=95=98=EB=82=98=EC=94=A9,=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20A=C2=B7B=EB=A1=9C=20=EB=B6=80=EB=A6=84=20(SIL-74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이슈로 묶어 "이슈 #7" 칩 하나에 다른 결정 사항이 섞이던 것을 결정 사항과 내 이슈 한 쌍마다 칩 하나로 나눔. 받은 순서대로 변경 A, 변경 B… 라서 답하거나 새 변경이 와도 이미 붙은 글자는 그대로예요. 마우스를 올리면 결정 사항 제목 · 이슈 · 상태 (app.detail.change-letter) Co-Authored-By: Claude Opus 5.5 --- .../src/screens/my-changes/ChangeDetail.tsx | 18 ++++++----- .../web/src/screens/my-changes/format.test.ts | 22 ++++++++++++++ apps/web/src/screens/my-changes/format.ts | 30 +++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/apps/web/src/screens/my-changes/ChangeDetail.tsx b/apps/web/src/screens/my-changes/ChangeDetail.tsx index b0edda2..8616a75 100644 --- a/apps/web/src/screens/my-changes/ChangeDetail.tsx +++ b/apps/web/src/screens/my-changes/ChangeDetail.tsx @@ -25,9 +25,10 @@ import { } from '../../ui/tds.tsx'; import { KindChips, SourceChips, StatusChip, statusLabel } from './Badges.tsx'; import { + changeLetter, findReplacingVersion, formatTime, - groupByIssue, + groupByChange, isNewStandard, needsRework, sameStandardVersions, @@ -261,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), ); @@ -304,24 +305,25 @@ 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 (