요청 2: 급한 순서를 정해야 하는 요청
요청문: src/stock.mjs에 todayTasks(items, today) 함수를 만들어 줘. 아침에 카페 사장님이 보는 '오늘 할 일' 목록이야. 유통기한이 지난 것, 3일 안에 끝나는 것, 수량이 minQty보다 적은 것을 한 목록으로 합쳐서 급한 것부터 보여 주고 싶어. today는 'YYYY-MM-DD' 문자열이야. test/stock.test.mjs에 테스트도 추가하고 돌려 봐.
diff --git a/src/stock.mjs b/src/stock.mjs
index f81b897..c4ddde8 100644
--- a/src/stock.mjs
+++ b/src/stock.mjs
@@ -19,3 +19,35 @@ 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;
+
+// 아침 '오늘 할 일': 재료당 한 줄, 급한 순서
+// 순서: 유통기한 지남(오래된 것 먼저) → 3일 안에 끝남(남은 날 적은 것 먼저) → 수량 부족만(재고 비율 낮은 것 먼저)
+export function todayTasks(items, today) {
+ const rank = { expired: 0, expiring: 1, low: 2 };
+ return items
+ .map((it) => {
+ const daysLeft = daysBetween(today, it.expires);
+ const reasons = [];
+ // 재고가 0이면 유통기한은 의미가 없음 (버리거나 빨리 쓸 게 없음)
+ if (it.qty > 0 && daysLeft < 0) reasons.push('expired');
+ else if (it.qty > 0 && daysLeft <= SOON_DAYS) reasons.push('expiring');
+ // 지난 재료는 버려야 하므로 실제 남는 수량은 0
+ const usableQty = reasons[0] === 'expired' ? 0 : it.qty;
+ if (usableQty < it.minQty) reasons.push('low');
+ return { name: it.name, reasons, daysLeft, qty: it.qty, minQty: it.minQty, unit: it.unit };
+ })
+ .filter((t) => t.reasons.length > 0)
+ .sort((a, b) => {
+ const ra = rank[a.reasons[0]];
+ const rb = rank[b.reasons[0]];
+ if (ra !== rb) return ra - rb;
+ if (ra < 2 && a.daysLeft !== b.daysLeft) return a.daysLeft - b.daysLeft;
+ if (ra === 2) {
+ const diff = a.qty / a.minQty - b.qty / b.minQty;
+ if (diff !== 0) return diff;
+ }
+ return a.name.localeCompare(b.name, 'ko');
+ });
+}
diff --git a/test/stock.test.mjs b/test/stock.test.mjs
index 71de632..5f87e65 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,43 @@ 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', 'low'], -1], // 어제 지남 → 버리면 0개라 부족도 함께
+ ['생크림', ['expiring'], 0], // 오늘까지 (0일 = 아직 지난 건 아님)
+ ['우유', ['expiring', 'low'], 2], // 한 재료가 두 사유여도 한 줄
+ ['바닐라 시럽', ['expiring'], 3], // 3일째 경계 포함
+ ['치즈케이크', ['low'], 1], // 0개면 유통기한 사유 없음
+ ],
+);
+assert.ok(!tasks.some((t) => t.name === '원두'), '문제없는 재료는 빠짐');
+
+// 경계: 4일 남음은 제외, qty === minQty는 부족 아님
+assert.deepEqual(
+ todayTasks([{ name: 'A', qty: 2, unit: '개', expires: '2026-10-22', minQty: 2 }], '2026-10-18'),
+ [],
+);
+
+// 지난 것끼리는 더 오래 지난 것 먼저, 부족만인 것끼리는 재고 비율 낮은 것 먼저
+const order = todayTasks(
+ [
+ { name: '부족-절반', qty: 5, unit: '개', expires: '2027-01-01', minQty: 10 },
+ { name: '지남-1일', qty: 1, unit: '개', expires: '2026-10-17', minQty: 0 },
+ { name: '부족-0개', qty: 0, unit: '개', expires: '2027-01-01', minQty: 3 },
+ { name: '지남-5일', qty: 1, unit: '개', expires: '2026-10-13', minQty: 0 },
+ ],
+ '2026-10-18',
+).map((t) => t.name);
+assert.deepEqual(order, ['지남-5일', '지남-1일', '부족-0개', '부족-절반']);
+
+// 원본 배열을 바꾸지 않음
+const copy = structuredClone(data);
+todayTasks(data, '2026-10-18');
+assert.deepEqual(data, copy);
+
+console.log('todayTasks 테스트 통과');