From 47b5eab3ff10e8bb626302ffef72ac0fae4a1af4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 27 May 2026 01:58:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(saju-ui-v2):=20=5Fshell/helpers=20?= =?UTF-8?q?=E2=80=94=20hexA/daeunLabel/deriveTraits/colorMap=20+=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/saju/_shell/helpers/colorMap.js | 20 ++++++++ src/pages/saju/_shell/helpers/daeunLabel.js | 10 ++++ src/pages/saju/_shell/helpers/deriveTraits.js | 31 ++++++++++++ src/pages/saju/_shell/helpers/helpers.test.js | 48 +++++++++++++++++++ src/pages/saju/_shell/helpers/hexA.js | 6 +++ 5 files changed, 115 insertions(+) create mode 100644 src/pages/saju/_shell/helpers/colorMap.js create mode 100644 src/pages/saju/_shell/helpers/daeunLabel.js create mode 100644 src/pages/saju/_shell/helpers/deriveTraits.js create mode 100644 src/pages/saju/_shell/helpers/helpers.test.js create mode 100644 src/pages/saju/_shell/helpers/hexA.js diff --git a/src/pages/saju/_shell/helpers/colorMap.js b/src/pages/saju/_shell/helpers/colorMap.js new file mode 100644 index 0000000..25a6501 --- /dev/null +++ b/src/pages/saju/_shell/helpers/colorMap.js @@ -0,0 +1,20 @@ +const ELEMENT_TO_VAR = { + wood: 'var(--el-wood)', + fire: 'var(--el-fire)', + earth: 'var(--el-earth)', + metal: 'var(--el-metal)', + water: 'var(--el-water)', +}; + +const ELEMENT_KO = { wood: '목', fire: '화', earth: '토', metal: '금', water: '수' }; +const ELEMENT_CH = { wood: '木', fire: '火', earth: '土', metal: '金', water: '水' }; + +export function elementColor(id) { + return ELEMENT_TO_VAR[id] || 'var(--navy)'; +} +export function elementKo(id) { + return ELEMENT_KO[id] || ''; +} +export function elementCh(id) { + return ELEMENT_CH[id] || ''; +} diff --git a/src/pages/saju/_shell/helpers/daeunLabel.js b/src/pages/saju/_shell/helpers/daeunLabel.js new file mode 100644 index 0000000..e4998f0 --- /dev/null +++ b/src/pages/saju/_shell/helpers/daeunLabel.js @@ -0,0 +1,10 @@ +export default function daeunLabel(age) { + if (age < 10) return '성장기'; + if (age < 20) return '학습기'; + if (age < 30) return '도전기'; + if (age < 40) return '성장기'; + if (age < 50) return '전성기'; + if (age < 60) return '안정기'; + if (age < 70) return '정리기'; + return '여유기'; +} diff --git a/src/pages/saju/_shell/helpers/deriveTraits.js b/src/pages/saju/_shell/helpers/deriveTraits.js new file mode 100644 index 0000000..733b649 --- /dev/null +++ b/src/pages/saju/_shell/helpers/deriveTraits.js @@ -0,0 +1,31 @@ +const TRAIT_DEFS = { + fire: { id: 'challenge', ko: '도전정신', icon: 'challenge', color: 'var(--el-fire)' }, + metal: { id: 'lead', ko: '리더십', icon: 'lead', color: 'var(--el-metal)' }, + wood: { id: 'adapt', ko: '적응력', icon: 'adapt', color: 'var(--el-wood)' }, + water: { id: 'wisdom', ko: '지혜', icon: 'wisdom', color: 'var(--el-water)' }, + earth: { id: 'wealth', ko: '풍부함', icon: 'wealth', color: 'var(--el-earth)' }, +}; + +const WILL_TRAIT = { id: 'will', ko: '의지', icon: 'will', color: 'var(--purple)' }; + +export default function deriveTraits(elements, sipsin = []) { + const sorted = Object.entries(elements || {}) + .filter(([, v]) => typeof v === 'number') + .sort((a, b) => b[1] - a[1]); + + const traits = []; + for (const [el, score] of sorted) { + if (score >= 30 && TRAIT_DEFS[el]) { + traits.push(TRAIT_DEFS[el]); + } + } + if (!traits.find((t) => t.id === 'will')) traits.push(WILL_TRAIT); + + for (const [el] of sorted) { + if (traits.length >= 6) break; + if (TRAIT_DEFS[el] && !traits.find((t) => t.id === TRAIT_DEFS[el].id)) { + traits.push(TRAIT_DEFS[el]); + } + } + return traits.slice(0, 6); +} diff --git a/src/pages/saju/_shell/helpers/helpers.test.js b/src/pages/saju/_shell/helpers/helpers.test.js new file mode 100644 index 0000000..5be6d37 --- /dev/null +++ b/src/pages/saju/_shell/helpers/helpers.test.js @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import hexA from './hexA'; +import daeunLabel from './daeunLabel'; +import deriveTraits from './deriveTraits'; +import { elementColor } from './colorMap'; + +describe('hexA', () => { + it('converts hex with alpha', () => { + expect(hexA('#1F2A44', 0.5)).toBe('rgba(31,42,68,0.5)'); + }); + it('handles 3-digit hex', () => { + expect(hexA('#abc', 1)).toBe('rgba(170,187,204,1)'); + }); +}); + +describe('daeunLabel', () => { + it('maps age ranges', () => { + expect(daeunLabel(5)).toBe('성장기'); + expect(daeunLabel(15)).toBe('학습기'); + expect(daeunLabel(25)).toBe('도전기'); + expect(daeunLabel(35)).toBe('성장기'); + expect(daeunLabel(45)).toBe('전성기'); + expect(daeunLabel(55)).toBe('안정기'); + expect(daeunLabel(65)).toBe('정리기'); + expect(daeunLabel(75)).toBe('여유기'); + }); +}); + +describe('deriveTraits', () => { + it('derives strong-element traits (sorted by score)', () => { + const traits = deriveTraits({ fire: 55, metal: 40, wood: 35, earth: 15, water: 20 }, []); + expect(traits.length).toBeLessThanOrEqual(6); + expect(traits[0].id).toBe('challenge'); + expect(traits.map((t) => t.id)).toContain('lead'); + }); + it('always includes will trait', () => { + const traits = deriveTraits({ fire: 50, metal: 30, wood: 30, earth: 30, water: 30 }, []); + expect(traits.map((t) => t.id)).toContain('will'); + }); +}); + +describe('elementColor', () => { + it('maps element ids to CSS vars', () => { + expect(elementColor('wood')).toBe('var(--el-wood)'); + expect(elementColor('fire')).toBe('var(--el-fire)'); + expect(elementColor('unknown')).toBe('var(--navy)'); + }); +}); diff --git a/src/pages/saju/_shell/helpers/hexA.js b/src/pages/saju/_shell/helpers/hexA.js new file mode 100644 index 0000000..55a2ae3 --- /dev/null +++ b/src/pages/saju/_shell/helpers/hexA.js @@ -0,0 +1,6 @@ +export default function hexA(hex, alpha) { + const h = hex.replace('#', ''); + const expanded = h.length === 3 ? h.split('').map((c) => c + c).join('') : h; + const n = parseInt(expanded, 16); + return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${alpha})`; +}