- 한국어 조사 분기: '보관로/휴지통로/활성로' → '보관으로/휴지통으로/활성으로'
('완료로' 만 받침 X). 받침 jongseong 검사 helper.
- MoveStatusModal 의 unused initialTarget prop 제거 + caller (NoteCard) 정리
548/548 + typecheck 0 유지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { inboxApi } from '../api.js';
|
|
import type { NoteStatus } from '@shared/types';
|
|
|
|
interface Props {
|
|
noteId: string;
|
|
rawText: string;
|
|
summary: string;
|
|
onClose: () => void;
|
|
onMoved: (status: NoteStatus, reason: string | null) => void;
|
|
}
|
|
|
|
/**
|
|
* v0.2.9 Cut B Task 7 — 메모 이동 Modal.
|
|
*
|
|
* 사유 입력 + 3 status 버튼 (완료/보관/휴지통) + AI 자동 분류.
|
|
*/
|
|
export function MoveStatusModal({
|
|
noteId,
|
|
onClose,
|
|
onMoved
|
|
}: Props): React.ReactElement {
|
|
const [reason, setReason] = useState('');
|
|
const [recommendation, setRecommendation] = useState<{
|
|
status: NoteStatus;
|
|
rationale: string;
|
|
} | null>(null);
|
|
const [classifying, setClassifying] = useState(false);
|
|
|
|
async function move(status: NoteStatus): Promise<void> {
|
|
const trimmedReason = reason.trim() === '' ? null : reason.trim();
|
|
await inboxApi.setStatus(noteId, status, trimmedReason);
|
|
onMoved(status, trimmedReason);
|
|
}
|
|
|
|
async function classify(): Promise<void> {
|
|
setClassifying(true);
|
|
setRecommendation(null);
|
|
try {
|
|
const r = await inboxApi.classifyStatus(noteId, reason);
|
|
setRecommendation({ status: r.recommended, rationale: r.rationale });
|
|
} finally {
|
|
setClassifying(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-label="이동"
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'rgba(0,0,0,0.4)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 100
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
background: '#fff',
|
|
padding: 16,
|
|
borderRadius: 8,
|
|
minWidth: 400,
|
|
maxWidth: 520
|
|
}}
|
|
>
|
|
<h2 style={{ fontSize: 16, margin: '0 0 12px' }}>메모 이동</h2>
|
|
<textarea
|
|
value={reason}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
placeholder="이동 사유 (선택사항)"
|
|
rows={2}
|
|
style={{ width: '100%', padding: 6, fontSize: 13, boxSizing: 'border-box' }}
|
|
/>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: 8,
|
|
marginTop: 8,
|
|
flexWrap: 'wrap',
|
|
alignItems: 'center'
|
|
}}
|
|
>
|
|
<button onClick={() => void classify()} disabled={classifying}>
|
|
{classifying ? '분류 중...' : 'AI 자동 분류'}
|
|
</button>
|
|
<button onClick={() => void move('completed')}>완료</button>
|
|
<button onClick={() => void move('archived')}>보관</button>
|
|
<button onClick={() => void move('trashed')}>휴지통</button>
|
|
<button onClick={onClose} style={{ marginLeft: 'auto' }}>
|
|
취소
|
|
</button>
|
|
</div>
|
|
{recommendation !== null && (
|
|
<div
|
|
style={{
|
|
marginTop: 12,
|
|
padding: 8,
|
|
background: '#f0f8ff',
|
|
borderRadius: 4,
|
|
fontSize: 12
|
|
}}
|
|
>
|
|
<div>
|
|
AI 추천: <strong>{statusLabel(recommendation.status)}</strong>
|
|
</div>
|
|
<div style={{ marginTop: 4 }}>이유: {recommendation.rationale}</div>
|
|
<div style={{ marginTop: 8 }}>
|
|
<button onClick={() => void move(recommendation.status)}>
|
|
확정 ({statusLabel(recommendation.status)})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function statusLabel(s: NoteStatus): string {
|
|
switch (s) {
|
|
case 'active':
|
|
return '활성';
|
|
case 'completed':
|
|
return '완료';
|
|
case 'archived':
|
|
return '보관';
|
|
case 'trashed':
|
|
return '휴지통';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* status 의 한글 라벨에 적절한 조사를 붙여 반환. 받침 있으면 "으로", 없으면 "로".
|
|
* 예: '완료로' / '보관으로' / '휴지통으로' / '활성으로'.
|
|
*/
|
|
export function statusLabelWithParticle(s: NoteStatus): string {
|
|
const label = statusLabel(s);
|
|
const last = label.charCodeAt(label.length - 1);
|
|
// 한글 syllable block 외 → "로" default
|
|
if (last < 0xAC00 || last > 0xD7A3) return `${label}로`;
|
|
const jongseong = (last - 0xAC00) % 28;
|
|
return jongseong === 0 ? `${label}로` : `${label}으로`;
|
|
}
|