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

learning-output-style 플러그인 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`를 돌려서 기존 테스트와 새 테스트가 모두 통과했습니다. `data.json`을 2026-10-18 기준으로 넣으면 이렇게 나옵니다. | 순서 | 재료 | 이유 | 남은 날 | |---|---|---|---| | 1 | 딸기 | 유통기한 지남 | -1 | | 2 | 생크림 | 곧 만료 | 0 (오늘) | | 3 | 치즈케이크 | 곧 만료 + 재고 부족 | 1 | | 4 | 우유 | 곧 만료 + 재고 부족 | 2 | | 5 | 바닐라 시럽 | 곧 만료 | 3 | 원두는 어느 조건에도 걸리지 않아 빠집니다. 항목마다 `{ name, reasons, daysLeft, qty, minQty, unit }`를 돌려주고, `reasons`에는 `'expired'`, `'expiresSoon'`, `'lowStock'`가 들어갑니다. `★ Insight ─────────────────────────────────────` - **급한 순서는 이렇게 정했습니다.** 지난 것 → 곧 만료 → 재고 부족만 있는 것 순입니다. 만료 문제는 오늘 처리하지 않으면 그대로 버리게 되고, 재고 부족은 주문하면 되기 때문입니다. 재고 부족끼리는 `qty / minQty` 비율이 작은 것부터 보여 줍니다. 그래서 하나도 없는 재료가 "1개 모자란" 재료보다 위에 옵니다. - **남은 날이 같으면 이유가 더 많은 재료가 위로 갑니다.** 예를 들어 같은 날 만료되면서 재고까지 부족한 재료가 만료만 걸린 재료보다 먼저 나옵니다. `─────────────────────────────────────────────────` **제가 임의로 정한 기준 두 가지입니다.** 사장님 운영 방식에 따라 바꾸셔도 됩니다. 1. **오늘 만료되는 재료**(생크림)는 "지남"이 아니라 "곧 만료(0일)"로 넣었습니다. 오늘까지는 쓸 수 있다고 봤습니다. 오늘 만료도 폐기 대상으로 보시려면 `daysLeft < 0`을 `<= 0`으로 바꾸면 됩니다. 2. **"3일 안"은 3일 뒤까지 포함**합니다(바닐라 시럽). 기준 일수는 `SOON_DAYS` 상수 하나로 바꿀 수 있습니다. 테스트에는 `data.json` 기준 순서, 오늘 만료와 3일 경계, 두 조건에 걸린 재료가 한 줄로만 나오는지, 재고 부족끼리의 순서, 빈 목록을 넣었습니다.

바뀐 코드 (git diff)

diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..02179ce 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,28 @@ export function orderList(items) {
     .filter((it) => it.qty < it.minQty)
     .map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
 }
+
+// 아침 '오늘 할 일' 목록. 한 재료가 여러 조건에 걸리면 한 줄로 묶고 reasons에 모두 담는다.
+// reasons: 'expired'(이미 지남) | 'expiresSoon'(오늘~3일 뒤 만료) | 'lowStock'(qty < minQty)
+// 정렬: 만료됨(오래된 순) → 곧 만료(남은 날 적은 순) → 재고 부족만(부족 비율 큰 순)
+export const SOON_DAYS = 3;
+
+export function todayTasks(items, today) {
+  const tasks = [];
+  for (const it of items) {
+    const daysLeft = daysBetween(today, it.expires);
+    const reasons = [];
+    if (daysLeft < 0) reasons.push('expired');
+    else if (daysLeft <= SOON_DAYS) reasons.push('expiresSoon');
+    if (it.qty < it.minQty) reasons.push('lowStock');
+    if (reasons.length === 0) continue;
+    tasks.push({ name: it.name, reasons, daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit });
+  }
+
+  const rank = (t) => (t.reasons.includes('expired') ? 0 : t.reasons.includes('expiresSoon') ? 1 : 2);
+  // 같은 그룹 안에서 작을수록 급함: 날짜 그룹은 남은 일수, 재고 그룹은 재고 비율(0이면 바닥)
+  const score = (t) => (rank(t) < 2 ? t.daysLeft : t.qty / t.minQty);
+  return tasks.sort(
+    (a, b) => rank(a) - rank(b) || score(a) - score(b) || b.reasons.length - a.reasons.length,
+  );
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..93e6267 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,30 @@ 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),
+  ['딸기', '생크림', '치즈케이크', '우유', '바닐라 시럽'],
+);
+assert.deepEqual(tasks[0].reasons, ['expired']); // 어제 만료
+assert.equal(tasks[1].daysLeft, 0); // 오늘 만료 = 아직 '곧 만료'
+assert.deepEqual(tasks[2].reasons, ['expiresSoon', 'lowStock']); // 두 조건 → 한 줄
+assert.equal(tasks.filter((t) => t.name === '우유').length, 1); // 중복 없음
+assert.equal(tasks[4].daysLeft, 3); // 3일 뒤까지 포함
+assert.ok(!tasks.some((t) => t.name === '원두')); // 해당 없음은 제외
+
+// 재고 부족만 있는 재료는 날짜 문제 뒤에, 부족 비율 큰 순
+const lowOnly = todayTasks(
+  [
+    { name: 'A', qty: 4, unit: 'kg', expires: '2027-01-01', minQty: 5 },
+    { name: 'B', qty: 0, unit: 'kg', expires: '2027-01-01', minQty: 5 },
+    { name: 'C', qty: 9, unit: 'kg', expires: '2026-10-20', minQty: 1 },
+  ],
+  '2026-10-18',
+);
+assert.deepEqual(lowOnly.map((t) => t.name), ['C', 'B', 'A']);
+assert.deepEqual(todayTasks([], '2026-10-18'), []);
+console.log('todayTasks 테스트 통과');