플러그인 없이 1회차 · 요청 2: 급한 순서를 정해야 하는 요청 · Claude Code · Opus 5.5 · 생각 깊이 중간 · 목록

플러그인 없이 1회차

요청 2: 급한 순서를 정해야 하는 요청

요청문: src/stock.mjs에 todayTasks(items, today) 함수를 만들어 줘. 아침에 카페 사장님이 보는 '오늘 할 일' 목록이야. 유통기한이 지난 것, 3일 안에 끝나는 것, 수량이 minQty보다 적은 것을 한 목록으로 합쳐서 급한 것부터 보여 주고 싶어. today는 'YYYY-MM-DD' 문자열이야. test/stock.test.mjs에 테스트도 추가하고 돌려 봐.

클로드의 마지막 답변 (그대로)

`src/stock.mjs`에 `todayTasks(items, today)`를 추가했고, `node test/stock.test.mjs`를 돌리면 기존 테스트와 새 테스트가 모두 통과합니다. **동작 방식** - 각 항목은 `{ name, reasons, label, daysLeft, qty, minQty, unit }` 형태입니다. `reasons`는 `'expired'`, `'expiring'`, `'low'` 중 해당하는 것들입니다. - 한 품목이 여러 조건에 걸려도 한 줄로 합칩니다. 예를 들어 우유는 `2일 남음, 재고 부족 (3/5L)`로 나옵니다. - **급한 순서:** 1. 유통기한이 지난 것. 오래 지난 것이 먼저입니다. 2. 3일 안에 끝나는 것. 임박한 것이 먼저이고, 날짜가 같으면 재고까지 부족한 품목이 먼저입니다. 3. 재고만 부족한 것. minQty 대비 남은 양이 적을수록 먼저입니다. `data.json`에 오늘 날짜(2026-10-18)를 넣으면 이렇게 나옵니다. | 순서 | 품목 | 표시 | |---|---|---| | 1 | 딸기 | 유통기한 1일 지남 | | 2 | 생크림 | 오늘까지 | | 3 | 우유 | 2일 남음, 재고 부족 (3/5L) | | 4 | 바닐라 시럽 | 3일 남음 | | 5 | 치즈케이크 | 재고 부족 (0/1판) | **제가 정한 기준 (다르게 원하시면 바꿀 수 있어요)** - "3일 안에"는 오늘부터 3일 남은 것까지 포함합니다. 4일 남은 것은 목록에서 빠집니다. - **수량이 0인 품목에는 유통기한 문구를 붙이지 않습니다.** 버리거나 먼저 쓸 재료가 없기 때문입니다. 그래서 치즈케이크는 유통기한이 내일까지인데도 재고 부족으로만 나옵니다. - **재고가 아예 없는 것보다 유통기한 문제를 더 급하게 봤습니다.** 재고가 0이어도 유통기한이 지나거나 임박한 품목보다 아래에 놓입니다. 오늘 장사 전에 재료를 사 와야 하는 쪽이 더 급하다고 보시면 순서를 바꿀 수 있습니다. 테스트 파일에는 `data.json`으로 위 순서를 확인하는 테스트와, 4일 남은 품목 제외·수량 0 처리·부족 비율 정렬·빈 목록을 확인하는 경계 테스트를 추가했습니다.

바뀐 코드 (git diff)

diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..7fe1bc7 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,40 @@ export function orderList(items) {
     .filter((it) => it.qty < it.minQty)
     .map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
 }
+
+// 아침 '오늘 할 일' 목록. 한 품목에 사유가 여러 개면 한 줄로 합친다.
+// 급한 순서: 유통기한 지남(오래된 것부터) → 3일 이내 만료(임박한 것부터) → 재고 부족만(부족 비율 큰 것부터)
+// 수량이 0이면 버리거나 먼저 쓸 것이 없으므로 유통기한 사유는 붙이지 않는다.
+const SOON_DAYS = 3;
+
+export function todayTasks(items, today) {
+  const tasks = [];
+  for (const it of items) {
+    const daysLeft = daysBetween(today, it.expires);
+    const reasons = [];
+    const labels = [];
+    if (it.qty > 0 && daysLeft < 0) {
+      reasons.push('expired');
+      labels.push(`유통기한 ${-daysLeft}일 지남`);
+    } else if (it.qty > 0 && daysLeft <= SOON_DAYS) {
+      reasons.push('expiring');
+      labels.push(daysLeft === 0 ? '오늘까지' : `${daysLeft}일 남음`);
+    }
+    if (it.qty < it.minQty) {
+      reasons.push('low');
+      labels.push(`재고 부족 (${it.qty}/${it.minQty}${it.unit})`);
+    }
+    if (reasons.length) {
+      tasks.push({ name: it.name, reasons, label: labels.join(', '), daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit });
+    }
+  }
+  const rank = (t) => (t.reasons.includes('expired') ? 0 : t.reasons.includes('expiring') ? 1 : 2);
+  return tasks.sort(
+    (a, b) =>
+      rank(a) - rank(b) ||
+      (rank(a) < 2 ? a.daysLeft - b.daysLeft : 0) ||
+      b.reasons.length - a.reasons.length ||
+      a.qty / a.minQty - b.qty / b.minQty ||
+      a.name.localeCompare(b.name),
+  );
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..8ae38bc 100644
--- a/test/stock.test.mjs
+++ b/test/stock.test.mjs
@@ -1,5 +1,6 @@
 import assert from 'node:assert/strict';
-import { lowStock, orderList, daysBetween } from '../src/stock.mjs';
+import { readFileSync } from 'node:fs';
+import { lowStock, orderList, daysBetween, todayTasks } from '../src/stock.mjs';
 const items = [
   { name: '우유', qty: 3, unit: 'L', expires: '2026-10-20', minQty: 5 },
   { name: '원두', qty: 4, unit: 'kg', expires: '2027-01-10', minQty: 2 },
@@ -8,3 +9,31 @@ assert.deepEqual(lowStock(items), ['우유']);
 assert.deepEqual(orderList(items), [{ name: '우유', need: 7, unit: 'L' }]);
 assert.equal(daysBetween('2026-10-18', '2026-10-20'), 2);
 console.log('기본 테스트 통과');
+
+// todayTasks: data.json 기준 2026-10-18 아침
+const data = JSON.parse(readFileSync(new URL('../data.json', import.meta.url), 'utf8'));
+const tasks = todayTasks(data, '2026-10-18');
+assert.deepEqual(
+  tasks.map((t) => [t.name, t.reasons, t.label]),
+  [
+    ['딸기', ['expired'], '유통기한 1일 지남'],
+    ['생크림', ['expiring'], '오늘까지'],
+    ['우유', ['expiring', 'low'], '2일 남음, 재고 부족 (3/5L)'],
+    ['바닐라 시럽', ['expiring'], '3일 남음'],
+    ['치즈케이크', ['low'], '재고 부족 (0/1판)'],
+  ],
+);
+
+// 경계: 4일 남으면 제외, 수량 0이면 유통기한 사유 없음, 재고 부족은 비율 큰 것부터
+const edge = todayTasks(
+  [
+    { name: 'A', qty: 5, unit: '개', expires: '2026-10-22', minQty: 1 },
+    { name: 'B', qty: 0, unit: '개', expires: '2026-10-10', minQty: 0 },
+    { name: 'C', qty: 3, unit: '개', expires: '2027-01-01', minQty: 4 },
+    { name: 'D', qty: 1, unit: '개', expires: '2027-01-01', minQty: 4 },
+  ],
+  '2026-10-18',
+);
+assert.deepEqual(edge.map((t) => t.name), ['D', 'C']);
+assert.deepEqual(todayTasks([], '2026-10-18'), []);
+console.log('todayTasks 테스트 통과');