내장 Learning 스타일 1회차 · 요청 3: 요청 2 + "핵심 부분은 직접 짜 보고 싶어" · Claude Code · Opus 5.5 · 생각 깊이 중간 · 목록

내장 Learning 스타일 1회차

요청 3: 요청 2 + "핵심 부분은 직접 짜 보고 싶어"

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

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

`★ Insight ─────────────────────────────────────` - 지금은 `compareTasks`가 `undefined`를 반환하는데도 테스트가 통과합니다. `sort`는 비교 결과가 숫자가 아니면 0, 즉 "순서 같음"으로 처리하고 원래 순서를 그대로 두기 때문이에요. 그래서 지금 테스트는 **무엇이 들어가는지**만 확인하고, **어떤 순서인지**는 아직 확인하지 않습니다. 순서 테스트는 함수를 다 짜고 나서 그 기준에 맞춰 추가하겠습니다. - 경계값은 이렇게 정했어요. 오늘이 유통기한인 날(0일 남음)과 3일 남은 날은 "곧 끝남"에 넣고, `qty === minQty`는 기존 `lowStock`과 똑같이 부족으로 보지 않습니다. `─────────────────────────────────────────────────` ● **Learn by Doing** **Context:** `todayTasks`의 틀은 다 만들어 두었습니다. 재료마다 `toTask`가 `{ name, reasons, daysLeft, qty, minQty, unit }` 형태의 할 일로 바꾸고, 이유가 하나도 없는 재료는 걸러지며, 남은 것들을 `compareTasks`로 정렬합니다. 테스트는 포함/제외 기준까지 통과했어요. 남은 건 **"무엇이 더 급한가"를 정하는 판단**인데, 이건 사장님의 실제 아침 업무를 정하는 비즈니스 로직이라 정답이 하나가 아닙니다. **Your Task:** `src/stock.mjs`의 `compareTasks(a, b)`에서 `TODO(human)` 부분을 채워 주세요. `a`가 더 급하면 음수, `b`가 더 급하면 양수, 똑같이 급하면 0을 반환하면 됩니다. 5~10줄 정도면 충분해요. **Guidance:** - `data.json`으로 직접 판단해 볼 수 있습니다(오늘 = 2026-10-18). - 딸기: 어제 유통기한 지남 (`expired`) - 생크림: 오늘까지 (`expiring`, daysLeft 0) - 치즈케이크: 재고 0 + 내일까지 (`expiring` + `low`) - 우유: 2일 남음 + 부족 (`expiring` + `low`) - 바닐라 시럽: 3일 남음 (`expiring`) - 고민할 점 1: **기한 지남 vs 재고 부족** 중 무엇을 먼저 볼까요? 지난 재료는 당장 버려야 하고(위생 문제), 부족한 재료는 주문해야 합니다(마감 시간이 있을 수도 있음). - 고민할 점 2: 같은 종류끼리는 `daysLeft`가 작은 쪽이 급할 거예요. 그럼 이유가 두 개인 우유가 이유가 하나인 생크림보다 먼저 와야 할까요? - 팁: 종류마다 점수를 매기는 작은 함수(예: `expired`=0, `expiring`=1, `low`=2)를 만들고 `a점수 - b점수`로 먼저 비교한 뒤, 같으면 `a.daysLeft - b.daysLeft`로 비교하는 방식이 깔끔합니다. 차이를 빼서 반환하는 이 방식은 숫자 비교 함수에서 자주 쓰는 관용구예요. - 다 짜면 말해 주세요. 그 순서를 확인하는 테스트를 추가하고 함께 돌려 보겠습니다.

바뀐 코드 (git diff)

diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..cfd19fd 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,27 @@ export function orderList(items) {
     .filter((it) => it.qty < it.minQty)
     .map((it) => ({ name: it.name, need: it.minQty * 2 - it.qty, unit: it.unit }));
 }
+
+export const SOON_DAYS = 3;
+
+// task: { name, reasons: ['expired' | 'expiring' | 'low'], daysLeft, qty, minQty, unit }
+export function toTask(it, today) {
+  const daysLeft = daysBetween(today, it.expires);
+  const reasons = [];
+  if (daysLeft < 0) reasons.push('expired');
+  else if (daysLeft <= SOON_DAYS) reasons.push('expiring');
+  if (it.qty < it.minQty) reasons.push('low');
+  return { name: it.name, reasons, daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit };
+}
+
+// 음수면 a가 먼저, 양수면 b가 먼저 (Array.prototype.sort 비교 함수 규칙)
+export function compareTasks(a, b) {
+  // TODO(human)
+}
+
+export function todayTasks(items, today) {
+  return items
+    .map((it) => toTask(it, today))
+    .filter((t) => t.reasons.length > 0)
+    .sort(compareTasks);
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..e05cea6 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, toTask, 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,24 @@ 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('기본 테스트 통과');
+
+// toTask: 이유 분류 (경계값 포함)
+const T = '2026-10-18';
+const item = (expires, qty = 5, minQty = 1) => ({ name: 'x', qty, unit: '개', expires, minQty });
+assert.deepEqual(toTask(item('2026-10-17'), T).reasons, ['expired']);
+assert.deepEqual(toTask(item('2026-10-18'), T).reasons, ['expiring']); // 오늘까지 = 0일 남음
+assert.deepEqual(toTask(item('2026-10-21'), T).reasons, ['expiring']); // 3일 남음은 포함
+assert.deepEqual(toTask(item('2026-10-22'), T).reasons, []);           // 4일 남음은 제외
+assert.deepEqual(toTask(item('2027-01-01', 1, 1), T).reasons, []);     // qty == minQty 는 부족 아님
+assert.deepEqual(toTask(item('2026-10-20', 0, 1), T).reasons, ['expiring', 'low']);
+console.log('toTask 테스트 통과');
+
+// todayTasks: 실제 데이터로 포함/제외 확인
+const data = JSON.parse(readFileSync(new URL('../data.json', import.meta.url)));
+const tasks = todayTasks(data, T);
+assert.deepEqual(
+  tasks.map((t) => t.name).sort(),
+  ['딸기', '바닐라 시럽', '생크림', '우유', '치즈케이크'].sort(),
+); // 원두는 빠져야 함
+assert.equal(todayTasks([], T).length, 0);
+console.log('todayTasks 포함 테스트 통과');