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

learning-output-style 플러그인 2회차

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

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

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

`src/stock.mjs`에 `todayTasks(items, today)`를 만들고 `test/stock.test.mjs`에 테스트를 추가해서 돌렸습니다. 기존 테스트와 새 테스트 모두 통과했어요. **`data.json`, 오늘 2026-10-18 기준 결과** | 순서 | 품목 | 사유 | 남은 날 | |---|---|---|---| | 1 | 딸기 | 유통기한 지남 | -1 | | 2 | 생크림 | 3일 안에 만료 | 0 | | 3 | 우유 | 3일 안에 만료 + 재고 부족 | 2 | | 4 | 바닐라 시럽 | 3일 안에 만료 | 3 | | 5 | 치즈케이크 | 재고 부족 | 1 | 원두는 문제가 없어서 목록에 나오지 않습니다. `★ Insight ─────────────────────────────────────` - **품목당 한 줄로 합쳤어요.** 우유처럼 사유가 두 개여도 한 줄에 `reasons: ['expiring', 'low']`로 담깁니다. 사유는 급한 순서로 들어가서, 첫 번째 사유가 곧 정렬 순위가 돼요. - **급한 순서는 이렇게 정했어요.** 유통기한 지남 → 3일 안에 만료 → 재고 부족 순입니다. 같은 단계 안에서 유통기한 문제는 남은 날이 적은 것부터, 재고 부족은 `qty / minQty` 비율이 작은 것부터 보여 줍니다. 수량 차이로 비교하면 우유 2L 부족과 시럽 1병 부족처럼 단위가 다른 품목끼리 비교할 수 없어서 비율을 썼어요. - **날짜 경계:** 오늘 끝나는 것(0일)과 딱 3일 남은 것도 "3일 안"에 넣었고, 4일 남은 것은 뺐습니다. 날짜 계산은 기존 `daysBetween`을 썼는데, 이 함수가 UTC 기준이라 시간대가 바뀌어도 하루씩 밀리지 않아요. `─────────────────────────────────────────────────` **제가 임의로 정한 규칙이 하나 있으니 확인해 주세요.** 수량이 0인 품목은 유통기한을 따지지 않습니다. 치즈케이크는 내일 만료지만 남은 게 없어 버릴 것도 없으니 "재고 부족"으로만 나와요. 사장님 입장에서 "0개인데 내일 만료"도 같이 보이는 게 낫다면 `if (it.qty > 0)` 조건만 지우면 됩니다. **직접 손대 볼 만한 부분:** 정렬 우선순위(`RANK`와 `sort` 비교 함수, 약 10줄)는 카페 운영 감각이 들어가야 하는 곳이에요. 예를 들어 "원두가 바닥난 게 우유 3일 남은 것보다 급하다"고 보신다면, 재고가 0인 품목을 맨 위로 올리는 식으로 바꿀 수 있습니다. 원하시면 그 부분을 TODO로 비워 둘 테니 직접 작성해 보세요.

바뀐 코드 (git diff)

diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..b766fdb 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,33 @@ export function orderList(items) {
     .filter((it) => it.qty < it.minQty)
     .map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
 }
+
+const SOON_DAYS = 3;
+const RANK = { expired: 0, expiring: 1, low: 2 };
+
+// 아침 '오늘 할 일': 유통기한 지남 / 3일 안에 만료 / minQty 미만을 품목당 한 줄로 합쳐 급한 순으로
+// 반환: [{ name, reasons: ['expired'|'expiring'|'low', ...], daysLeft, qty, minQty, unit }]
+export function todayTasks(items, today) {
+  const tasks = [];
+  for (const it of items) {
+    const daysLeft = daysBetween(today, it.expires);
+    const reasons = [];
+    // 남은 수량이 없으면 버릴 것도 없으니 유통기한은 따지지 않음
+    if (it.qty > 0) {
+      if (daysLeft < 0) reasons.push('expired');
+      else if (daysLeft <= SOON_DAYS) reasons.push('expiring');
+    }
+    if (it.qty < it.minQty) reasons.push('low');
+    if (reasons.length) {
+      tasks.push({ name: it.name, reasons, daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit });
+    }
+  }
+  return tasks.sort((a, b) => {
+    const ra = RANK[a.reasons[0]];
+    const rb = RANK[b.reasons[0]];
+    if (ra !== rb) return ra - rb;
+    // 유통기한 문제끼리는 남은 날짜가 적은 순, 재고 부족끼리는 모자란 비율이 큰 순
+    if (ra < RANK.low) return a.daysLeft - b.daysLeft;
+    return a.qty / a.minQty - b.qty / b.minQty;
+  });
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..1be585c 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,35 @@ 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.daysLeft]),
+  [
+    ['딸기', ['expired'], -1], // 어제 지남 → 가장 급함
+    ['생크림', ['expiring'], 0], // 오늘까지
+    ['우유', ['expiring', 'low'], 2], // 두 사유를 한 줄로 합침
+    ['바닐라 시럽', ['expiring'], 3], // 3일째 경계 포함
+    ['치즈케이크', ['low'], 1], // 수량 0 → 유통기한은 무시, 재고 부족만
+  ],
+);
+assert.ok(!tasks.some((t) => t.name === '원두')); // 문제없는 품목은 빠짐
+
+// 재고 부족끼리는 모자란 비율이 큰 순
+const lows = todayTasks(
+  [
+    { name: 'A', qty: 4, unit: '개', expires: '2027-01-01', minQty: 5 },
+    { name: 'B', qty: 1, unit: '개', expires: '2027-01-01', minQty: 5 },
+  ],
+  '2026-10-18',
+);
+assert.deepEqual(lows.map((t) => t.name), ['B', 'A']);
+
+// 4일 남은 것은 아직 할 일 아님
+assert.deepEqual(
+  todayTasks([{ name: 'C', qty: 3, unit: '개', expires: '2026-10-22', minQty: 1 }], '2026-10-18'),
+  [],
+);
+console.log('todayTasks 테스트 통과');