From 0a4f46b073b4e8b573dc2015422d4064a290e616 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Tue, 23 Dec 2025 23:50:21 -0500 Subject: [PATCH 1/5] fix: dynamic size bug and apply it to receive page --- libs/leon-sans-react/src/domain/WreathSansController.ts | 5 +++-- libs/leon-sans-react/src/hooks/createWreathSans.tsx | 6 ++++++ projects/waffle-sans/src/components/ReceivedContent.tsx | 2 ++ projects/waffle-sans/src/hooks/useWreathSans.ts | 8 ++++++++ projects/waffle-sans/src/pages/InsideSans.tsx | 2 +- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/libs/leon-sans-react/src/domain/WreathSansController.ts b/libs/leon-sans-react/src/domain/WreathSansController.ts index ae5223e8..11ac30ff 100644 --- a/libs/leon-sans-react/src/domain/WreathSansController.ts +++ b/libs/leon-sans-react/src/domain/WreathSansController.ts @@ -99,11 +99,12 @@ export default class WreathSansController { }: WreathSansProps) { const canvasWidth = canvas?.clientWidth ?? 300; const canvasHeight = canvas?.clientHeight ?? 300; + const initialSize = dynamicSize ? (leonOptions?.size ?? 130) : 0; this.leon = new LeonSans({ text: initialText, color: [leonOptions?.color ?? '#704234'], - size: 0, + size: initialSize, weight: leonOptions?.weight ?? 400, isPattern: true, pathGap: leonOptions?.pathGap ?? 1 / 20, @@ -373,7 +374,7 @@ export default class WreathSansController { private dynamicResize() { if (this.leon.rect.w === 0) return; const newSize = this.size * (this.canvas.clientWidth / this.leon.rect.w); - if (newSize >= this._minSize) this.size = newSize; + this.size = Math.max(newSize, this._minSize); } private async loadAssets() { diff --git a/libs/leon-sans-react/src/hooks/createWreathSans.tsx b/libs/leon-sans-react/src/hooks/createWreathSans.tsx index 29d7aae7..83dd7aa1 100644 --- a/libs/leon-sans-react/src/hooks/createWreathSans.tsx +++ b/libs/leon-sans-react/src/hooks/createWreathSans.tsx @@ -16,6 +16,8 @@ type Props = { background?: string; backgroundAlpha?: number; darkMode?: boolean; + fitToWidth?: boolean; + minSize?: number; }; export default function createWreathSans({ @@ -28,12 +30,16 @@ export default function createWreathSans({ background = '#ffffff', backgroundAlpha = 1, darkMode, + fitToWidth = false, + minSize, }: Props) { const wreathSansController = new WreathSansController({ initialText, background, backgroundAlpha, darkMode, + dynamicSize: fitToWidth, + minSize, leonOptions: { color, size, diff --git a/projects/waffle-sans/src/components/ReceivedContent.tsx b/projects/waffle-sans/src/components/ReceivedContent.tsx index 5fec2481..03c34d9b 100644 --- a/projects/waffle-sans/src/components/ReceivedContent.tsx +++ b/projects/waffle-sans/src/components/ReceivedContent.tsx @@ -27,6 +27,8 @@ export default function ReceivedContent({ initialText: sans, darkMode: mode === 'o', fontColor: mode === 'o' ? '#704234' : '#B27E41', + fitToWidth: true, + minSize: 0, }); useEffect(() => { diff --git a/projects/waffle-sans/src/hooks/useWreathSans.ts b/projects/waffle-sans/src/hooks/useWreathSans.ts index 62b182a3..f9c3ad74 100644 --- a/projects/waffle-sans/src/hooks/useWreathSans.ts +++ b/projects/waffle-sans/src/hooks/useWreathSans.ts @@ -8,6 +8,8 @@ interface Params { fontColor?: string; darkMode?: boolean; initialText: string; + fitToWidth?: boolean; + minSize?: number; } export default function useWreathSans({ @@ -16,6 +18,8 @@ export default function useWreathSans({ darkMode, fontColor, initialText, + fitToWidth, + minSize, }: Params) { const ref = useRef(null); @@ -30,6 +34,8 @@ export default function useWreathSans({ background: 'transparent', backgroundAlpha: 0, darkMode: darkMode ?? false, + fitToWidth, + minSize, }); }, [ initialText, @@ -37,6 +43,8 @@ export default function useWreathSans({ height, fontColor, darkMode, + fitToWidth, + minSize, ref.current?.offsetWidth, ref.current?.offsetHeight, ]); diff --git a/projects/waffle-sans/src/pages/InsideSans.tsx b/projects/waffle-sans/src/pages/InsideSans.tsx index c62f112a..ce34f926 100644 --- a/projects/waffle-sans/src/pages/InsideSans.tsx +++ b/projects/waffle-sans/src/pages/InsideSans.tsx @@ -9,7 +9,7 @@ import SnowFlakes from '../components/SnowFlakes'; import { GRID } from '../constants/breakpoint'; import { Mode } from '../types/mode'; -export default function OutsideSans() { +export default function InsideSans() { const router = useNavigate(); return ( From fc16740ae69b0df878a607ca1b0096af80d69e76 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Wed, 24 Dec 2025 00:08:08 -0500 Subject: [PATCH 2/5] feature: align waffle sans --- .../src/domain/WreathSansController.ts | 3 ++ .../src/hooks/createWreathSans.tsx | 10 +++- .../waffle-sans/src/components/Letter.tsx | 10 +++- .../src/components/PostPreview.tsx | 5 ++ .../src/components/ReceivedContent.tsx | 8 ++++ .../waffle-sans/src/components/SansForm.tsx | 41 ++++++++++++----- .../waffle-sans/src/hooks/useWreathSans.ts | 16 ++++++- projects/waffle-sans/src/pages/InsideSans.tsx | 46 ++++++++++++++++++- .../waffle-sans/src/pages/OutsideSans.tsx | 46 ++++++++++++++++++- projects/waffle-sans/src/pages/Receive.tsx | 10 +++- projects/waffle-sans/src/utils/crypto.ts | 2 + 11 files changed, 177 insertions(+), 20 deletions(-) diff --git a/libs/leon-sans-react/src/domain/WreathSansController.ts b/libs/leon-sans-react/src/domain/WreathSansController.ts index 11ac30ff..062ce23f 100644 --- a/libs/leon-sans-react/src/domain/WreathSansController.ts +++ b/libs/leon-sans-react/src/domain/WreathSansController.ts @@ -42,6 +42,7 @@ type LeonOptions = { size?: number; weight?: number; pathGap?: number; + align?: Align; }; type WreathSansProps = { @@ -108,6 +109,7 @@ export default class WreathSansController { weight: leonOptions?.weight ?? 400, isPattern: true, pathGap: leonOptions?.pathGap ?? 1 / 20, + align: leonOptions?.align ?? 'left', }); this.leon.update(); @@ -179,6 +181,7 @@ export default class WreathSansController { set align(align: Align) { this.leon.align = align; + this.leon.updateDrawingPaths(); this.updatePositions(); } diff --git a/libs/leon-sans-react/src/hooks/createWreathSans.tsx b/libs/leon-sans-react/src/hooks/createWreathSans.tsx index 83dd7aa1..c3d12b72 100644 --- a/libs/leon-sans-react/src/hooks/createWreathSans.tsx +++ b/libs/leon-sans-react/src/hooks/createWreathSans.tsx @@ -1,4 +1,4 @@ -import { CHARSET } from 'leonsans'; +import { Align, CHARSET } from 'leonsans'; import { ComponentPropsWithoutRef } from 'react'; import WreathSansController from '../domain/WreathSansController'; @@ -9,6 +9,7 @@ type Props = { color?: string; size?: number; weight?: number; + align?: Align; // canvas config width?: number; height?: number; @@ -25,6 +26,7 @@ export default function createWreathSans({ color = '#000000', size = 60, weight = 400, + align = 'left', width = 800, height = 600, background = '#ffffff', @@ -44,6 +46,7 @@ export default function createWreathSans({ color, size, weight, + align, }, }); @@ -140,11 +143,16 @@ export default function createWreathSans({ return wreathSansController.leon.text; } + function setAlign(newAlign: Align) { + wreathSansController.align = newAlign; + } + return { WreathSansCanvas, onInputHandler, resize, redraw, getText, + setAlign, }; } diff --git a/projects/waffle-sans/src/components/Letter.tsx b/projects/waffle-sans/src/components/Letter.tsx index 297354de..98035089 100644 --- a/projects/waffle-sans/src/components/Letter.tsx +++ b/projects/waffle-sans/src/components/Letter.tsx @@ -12,9 +12,16 @@ type LetterProps = { content: string; sans: string; mode: string; + align?: string; }; -export default function Letter({ sender, content, sans, mode }: LetterProps) { +export default function Letter({ + sender, + content, + sans, + mode, + align, +}: LetterProps) { const [stage, setStage] = useState<(typeof stages)[number]>('shake'); const parsedMode = useMemo( () => (mode === 'o' ? 'outside' : 'inside'), @@ -45,6 +52,7 @@ export default function Letter({ sender, content, sans, mode }: LetterProps) { content={content} mode={mode} stage={stage} + align={align} /> new URL(window.location.href), []); const sans = useMemo(() => decoder(url, 'sans'), [url]); + const align = useMemo(() => decoder(url, 'align'), [url]); const { ref, WreathSansCanvas } = useWreathSans({ initialText: sans, darkMode: mode === Mode.OUTSIDE, fontColor: mode === Mode.OUTSIDE ? '#704234' : '#B27E41', + align: + align === 'left' || align === 'center' || align === 'right' + ? align + : undefined, }); return ( diff --git a/projects/waffle-sans/src/components/ReceivedContent.tsx b/projects/waffle-sans/src/components/ReceivedContent.tsx index 03c34d9b..6a8918d9 100644 --- a/projects/waffle-sans/src/components/ReceivedContent.tsx +++ b/projects/waffle-sans/src/components/ReceivedContent.tsx @@ -9,6 +9,7 @@ type ReceivedContentProps = { sans: string; mode: string; stage?: string; + align?: string; }; export default function ReceivedContent({ @@ -17,9 +18,12 @@ export default function ReceivedContent({ sans, mode, stage, + align, }: ReceivedContentProps) { const [width, setWidth] = useState(280); const [height, setHeight] = useState(196); + console.log(align) + align = align ?? 'center'; const { ref, WreathSansCanvas, redraw } = useWreathSans({ width, @@ -29,6 +33,10 @@ export default function ReceivedContent({ fontColor: mode === 'o' ? '#704234' : '#B27E41', fitToWidth: true, minSize: 0, + align: + align === 'left' || align === 'center' || align === 'right' + ? align + : undefined, }); useEffect(() => { diff --git a/projects/waffle-sans/src/components/SansForm.tsx b/projects/waffle-sans/src/components/SansForm.tsx index c469fe0c..615e1a7f 100644 --- a/projects/waffle-sans/src/components/SansForm.tsx +++ b/projects/waffle-sans/src/components/SansForm.tsx @@ -14,19 +14,27 @@ import Textarea from './Textarea'; interface Props { mode?: Mode; + align?: 'left' | 'center' | 'right'; } -export default function SansForm({ mode = Mode.OUTSIDE }: Props) { +export default function SansForm({ mode = Mode.OUTSIDE, align }: Props) { const router = useNavigate(); const defaultValue = useMemo(() => 'interactive study', []); - const { ref, WreathSansCanvas, redraw, resize, getText, onInputHandler } = - useWreathSans({ - width: window.innerWidth, - height: (window.innerHeight / 100) * 62, - initialText: defaultValue, - darkMode: mode === Mode.OUTSIDE, - fontColor: mode === Mode.OUTSIDE ? '#704234' : '#B27E41', - }); + const { + ref, + WreathSansCanvas, + redraw, + resize, + getText, + onInputHandler, + setAlign, + } = useWreathSans({ + width: window.innerWidth, + height: (window.innerHeight / 100) * 62, + initialText: defaultValue, + darkMode: mode === Mode.OUTSIDE, + fontColor: mode === Mode.OUTSIDE ? '#704234' : '#B27E41', + }); const handleShare = useCallback(() => { if (!getText().trim()) { @@ -34,10 +42,15 @@ export default function SansForm({ mode = Mode.OUTSIDE }: Props) { return; } const text = encoder(getText()); + const encodedAlign = encoder(align ?? 'left'); mode === Mode.OUTSIDE - ? router(`/o-post?sans=${text}&mode=${encoder('o')}`) - : router(`/i-post?sans=${text}&mode=${encoder('i')}`); - }, [getText, mode, router]); + ? router( + `/o-post?sans=${text}&mode=${encoder('o')}&align=${encodedAlign}`, + ) + : router( + `/i-post?sans=${text}&mode=${encoder('i')}&align=${encodedAlign}`, + ); + }, [align, getText, mode, router]); useEffect(() => { function handleResize() { @@ -49,6 +62,10 @@ export default function SansForm({ mode = Mode.OUTSIDE }: Props) { }; }, [resize]); + useEffect(() => { + if (align) setAlign(align); + }, [align, setAlign]); + return ( diff --git a/projects/waffle-sans/src/hooks/useWreathSans.ts b/projects/waffle-sans/src/hooks/useWreathSans.ts index f9c3ad74..f725f727 100644 --- a/projects/waffle-sans/src/hooks/useWreathSans.ts +++ b/projects/waffle-sans/src/hooks/useWreathSans.ts @@ -10,6 +10,7 @@ interface Params { initialText: string; fitToWidth?: boolean; minSize?: number; + align?: 'left' | 'center' | 'right'; } export default function useWreathSans({ @@ -20,10 +21,11 @@ export default function useWreathSans({ initialText, fitToWidth, minSize, + align, }: Params) { const ref = useRef(null); - const { WreathSansCanvas, resize, redraw, getText, onInputHandler } = + const { WreathSansCanvas, resize, redraw, getText, onInputHandler, setAlign } = useMemo(() => { return createWreathSans({ initialText: initialText, @@ -36,6 +38,7 @@ export default function useWreathSans({ darkMode: darkMode ?? false, fitToWidth, minSize, + align, }); }, [ initialText, @@ -45,6 +48,7 @@ export default function useWreathSans({ darkMode, fitToWidth, minSize, + align, ref.current?.offsetWidth, ref.current?.offsetHeight, ]); @@ -62,5 +66,13 @@ export default function useWreathSans({ }; }, [redraw, resize, ref]); - return { WreathSansCanvas, ref, resize, redraw, getText, onInputHandler }; + return { + WreathSansCanvas, + ref, + resize, + redraw, + getText, + onInputHandler, + setAlign, + }; } diff --git a/projects/waffle-sans/src/pages/InsideSans.tsx b/projects/waffle-sans/src/pages/InsideSans.tsx index ce34f926..28325528 100644 --- a/projects/waffle-sans/src/pages/InsideSans.tsx +++ b/projects/waffle-sans/src/pages/InsideSans.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -11,6 +12,7 @@ import { Mode } from '../types/mode'; export default function InsideSans() { const router = useNavigate(); + const [align, setAlign] = useState<'left' | 'center' | 'right'>('left'); return ( @@ -19,7 +21,30 @@ export default function InsideSans() {
- + + setAlign('left')} + > + LEFT + + setAlign('center')} + > + CENTER + + setAlign('right')} + > + RIGHT + + + @@ -95,6 +120,25 @@ const Content = styled.div` } `; +const AlignControls = styled.div` + display: flex; + gap: 8px; + margin-bottom: 8px; +`; + +const AlignButton = styled.button<{ $active: boolean }>` + appearance: none; + border: 1px solid ${({ $active }) => ($active ? '#fff' : '#d8c7b8')}; + background: ${({ $active }) => ($active ? '#ffffff' : 'transparent')}; + color: ${({ $active }) => ($active ? '#5e3517' : '#fff')}; + font-family: inherit; + font-size: 12px; + letter-spacing: 0.08em; + padding: 6px 10px; + border-radius: 999px; + cursor: pointer; +`; + const BackBtnContainer = styled.div` width: auto; height: auto; diff --git a/projects/waffle-sans/src/pages/OutsideSans.tsx b/projects/waffle-sans/src/pages/OutsideSans.tsx index 57dda05d..ebe2d98b 100644 --- a/projects/waffle-sans/src/pages/OutsideSans.tsx +++ b/projects/waffle-sans/src/pages/OutsideSans.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -9,12 +10,36 @@ import { GRID } from '../constants/breakpoint'; export default function OutsideSans() { const router = useNavigate(); + const [align, setAlign] = useState<'left' | 'center' | 'right'>('left'); return (
- + + setAlign('left')} + > + LEFT + + setAlign('center')} + > + CENTER + + setAlign('right')} + > + RIGHT + + + @@ -62,6 +87,25 @@ const Content = styled.div` } `; +const AlignControls = styled.div` + display: flex; + gap: 8px; + margin-bottom: 8px; +`; + +const AlignButton = styled.button<{ $active: boolean }>` + appearance: none; + border: 1px solid ${({ $active }) => ($active ? '#2e3a2c' : '#c6d7c5')}; + background: ${({ $active }) => ($active ? '#2e3a2c' : 'transparent')}; + color: ${({ $active }) => ($active ? '#ffffff' : '#2e3a2c')}; + font-family: inherit; + font-size: 12px; + letter-spacing: 0.08em; + padding: 6px 10px; + border-radius: 999px; + cursor: pointer; +`; + const BackBtnContainer = styled.div` position: absolute; width: auto; diff --git a/projects/waffle-sans/src/pages/Receive.tsx b/projects/waffle-sans/src/pages/Receive.tsx index 802da7e2..96465303 100644 --- a/projects/waffle-sans/src/pages/Receive.tsx +++ b/projects/waffle-sans/src/pages/Receive.tsx @@ -6,7 +6,7 @@ import MobileFooter from '../components/MobileFooter'; import { decodeParams } from '../utils/crypto'; export default function Receive() { - const { sender, content, receiver, sans, mode } = useMemo( + const { sender, content, receiver, sans, mode, align } = useMemo( () => decodeParams(new URL(window.location.href)), [], ); @@ -16,7 +16,13 @@ export default function Receive() {
{receiver}님에게 편지가 왔어요! - + diff --git a/projects/waffle-sans/src/utils/crypto.ts b/projects/waffle-sans/src/utils/crypto.ts index f9d621ba..09ab4188 100644 --- a/projects/waffle-sans/src/utils/crypto.ts +++ b/projects/waffle-sans/src/utils/crypto.ts @@ -21,6 +21,7 @@ export const decodeParams = (url: URL) => { const decodedReceiver = decoder(url, 'receiver'); const decodedSans = decoder(url, 'sans'); const decodedMode = decoder(url, 'mode'); + const decodedAlign = decoder(url, 'align'); return { sender: decodedSender, @@ -28,5 +29,6 @@ export const decodeParams = (url: URL) => { receiver: decodedReceiver, sans: decodedSans, mode: decodedMode, + align: decodedAlign, }; }; From ef4bfd327828cf351fbc6b337142ed8026600dd0 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Wed, 24 Dec 2025 00:21:51 -0500 Subject: [PATCH 3/5] feat: dynamic height adjustments --- .../src/domain/WreathSansController.ts | 11 ++- .../src/hooks/createWreathSans.tsx | 5 ++ .../waffle-sans/src/components/Letter.tsx | 16 +++- .../src/components/ReceivedContent.tsx | 83 +++++++++++++++---- .../waffle-sans/src/hooks/useWreathSans.ts | 56 +++++++------ 5 files changed, 127 insertions(+), 44 deletions(-) diff --git a/libs/leon-sans-react/src/domain/WreathSansController.ts b/libs/leon-sans-react/src/domain/WreathSansController.ts index 062ce23f..32c602a5 100644 --- a/libs/leon-sans-react/src/domain/WreathSansController.ts +++ b/libs/leon-sans-react/src/domain/WreathSansController.ts @@ -1,5 +1,5 @@ import gsap, { Power0, Power3 } from 'gsap'; -import { Align, CHARSET, ModelData } from 'leonsans'; +import { Align, CHARSET, ModelData, Rect } from 'leonsans'; import LeonSans from 'leonsans'; import * as PIXI from 'pixi.js'; @@ -344,6 +344,15 @@ export default class WreathSansController { }); } + getTextRect(): Rect { + return { + x: this.leon.rect.x, + y: this.leon.rect.y, + w: this.leon.rect.w, + h: this.leon.rect.h, + }; + } + /** * leonsans의 데이터를 사용하여 글자들의 위치를 업데이트한다. * 그러나 leonsans에는 영향을 주지 않는다. diff --git a/libs/leon-sans-react/src/hooks/createWreathSans.tsx b/libs/leon-sans-react/src/hooks/createWreathSans.tsx index c3d12b72..50bac20f 100644 --- a/libs/leon-sans-react/src/hooks/createWreathSans.tsx +++ b/libs/leon-sans-react/src/hooks/createWreathSans.tsx @@ -147,6 +147,10 @@ export default function createWreathSans({ wreathSansController.align = newAlign; } + function getTextRect() { + return wreathSansController.getTextRect(); + } + return { WreathSansCanvas, onInputHandler, @@ -154,5 +158,6 @@ export default function createWreathSans({ redraw, getText, setAlign, + getTextRect, }; } diff --git a/projects/waffle-sans/src/components/Letter.tsx b/projects/waffle-sans/src/components/Letter.tsx index 98035089..9cbbef4e 100644 --- a/projects/waffle-sans/src/components/Letter.tsx +++ b/projects/waffle-sans/src/components/Letter.tsx @@ -23,10 +23,16 @@ export default function Letter({ align, }: LetterProps) { const [stage, setStage] = useState<(typeof stages)[number]>('shake'); + const [contentHeight, setContentHeight] = useState(null); const parsedMode = useMemo( () => (mode === 'o' ? 'outside' : 'inside'), [mode], ); + const baseHeight = stage === 'out' ? 652 : 252; + const adjustedHeight = + stage === 'out' && contentHeight + ? Math.max(baseHeight, contentHeight) + : baseHeight; const onClickLetter = useCallback(() => { const currentStageIndex = stages.findIndex((s) => s === stage); @@ -40,7 +46,7 @@ export default function Letter({ $isOutside={parsedMode === 'outside'} $isOut={stage === 'out'} /> - + ` +const Container = styled.div<{ + $stage: 'shake' | 'close' | 'open' | 'out'; + $height: number; +}>` position: relative; width: 100%; - height: ${({ $stage }) => ($stage === 'out' ? '652px' : '252px')}; + height: ${({ $height }) => `${$height}px`}; transition: 1s ease; diff --git a/projects/waffle-sans/src/components/ReceivedContent.tsx b/projects/waffle-sans/src/components/ReceivedContent.tsx index 6a8918d9..16b4cbcc 100644 --- a/projects/waffle-sans/src/components/ReceivedContent.tsx +++ b/projects/waffle-sans/src/components/ReceivedContent.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import useWreathSans from '../hooks/useWreathSans'; @@ -10,6 +10,7 @@ type ReceivedContentProps = { mode: string; stage?: string; align?: string; + onHeightChange?: (height: number) => void; }; export default function ReceivedContent({ @@ -19,13 +20,20 @@ export default function ReceivedContent({ mode, stage, align, + onHeightChange, }: ReceivedContentProps) { const [width, setWidth] = useState(280); const [height, setHeight] = useState(196); - console.log(align) - align = align ?? 'center'; + const [baseHeight, setBaseHeight] = useState(null); + const [containerHeight, setContainerHeight] = useState(null); + const [sansHeight, setSansHeight] = useState(null); + const containerRef = useRef(null); + const baseSansHeight = useMemo( + () => (baseHeight ? baseHeight * 0.3 : null), + [baseHeight], + ); - const { ref, WreathSansCanvas, redraw } = useWreathSans({ + const { ref, WreathSansCanvas, redraw, resize, getTextRect } = useWreathSans({ width, height, initialText: sans, @@ -40,20 +48,62 @@ export default function ReceivedContent({ }); useEffect(() => { - if (ref?.current) { - setWidth(ref?.current?.offsetWidth); - setHeight(ref.current.offsetHeight); - redraw(); - } - }, [redraw, ref]); + const updateBaseSize = () => { + if (!containerRef.current) return; + const containerWidth = containerRef.current.offsetWidth; + setBaseHeight(containerWidth / 0.55); + if (ref.current) { + const wrapperWidth = ref.current.offsetWidth; + setWidth(wrapperWidth); + } + }; + updateBaseSize(); + window.addEventListener('resize', updateBaseSize); + return () => { + window.removeEventListener('resize', updateBaseSize); + }; + }, []); + + useEffect(() => { + if (!baseHeight) return; + const updateSansHeight = () => { + const rect = getTextRect(); + if (!rect || rect.h === 0) return; + const targetHeight = Math.ceil(rect.h + 12); + const minHeight = baseSansHeight ?? 0; + setSansHeight(Math.max(targetHeight, minHeight)); + }; + const rafId = requestAnimationFrame(updateSansHeight); + return () => cancelAnimationFrame(rafId); + }, [baseHeight, baseSansHeight, getTextRect, sans, stage]); + + useEffect(() => { + if (!baseHeight || !baseSansHeight) return; + const nextSansHeight = sansHeight ?? baseSansHeight; + const delta = Math.max(0, nextSansHeight - baseSansHeight); + const nextHeight = Math.ceil(baseHeight + delta); + setContainerHeight(nextHeight); + onHeightChange?.(nextHeight); + }, [baseHeight, baseSansHeight, onHeightChange, sansHeight]); + + useEffect(() => { + if (!sansHeight || !width) return; + setHeight(sansHeight); + resize(width, sansHeight); + redraw(); + }, [redraw, resize, sansHeight, width]); useEffect(() => { redraw(); }, [redraw, stage]); return ( - - + + {content} @@ -63,11 +113,12 @@ export default function ReceivedContent({ ); } -const Container = styled.div<{ $isOutside: boolean }>` +const Container = styled.div<{ $isOutside: boolean; $height: number | null }>` position: relative; top: 10px; width: 100%; - aspect-ratio: 0.55; + height: ${({ $height }) => ($height ? `${$height}px` : 'auto')}; + aspect-ratio: ${({ $height }) => ($height ? 'auto' : '0.55')}; box-sizing: border-box; padding: 45px 40px; @@ -85,9 +136,9 @@ const Container = styled.div<{ $isOutside: boolean }>` box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.15); `; -const SansWrapper = styled.div` +const SansWrapper = styled.div<{ $height: number | null }>` width: 100%; - height: 30%; + height: ${({ $height }) => ($height ? `${$height}px` : '30%')}; `; const MainText = styled.div<{ $isOutside: boolean }>` diff --git a/projects/waffle-sans/src/hooks/useWreathSans.ts b/projects/waffle-sans/src/hooks/useWreathSans.ts index f725f727..d7439242 100644 --- a/projects/waffle-sans/src/hooks/useWreathSans.ts +++ b/projects/waffle-sans/src/hooks/useWreathSans.ts @@ -25,33 +25,40 @@ export default function useWreathSans({ }: Params) { const ref = useRef(null); - const { WreathSansCanvas, resize, redraw, getText, onInputHandler, setAlign } = - useMemo(() => { - return createWreathSans({ - initialText: initialText, - width: width ? width : ref.current?.offsetWidth ?? 330, - height: height ? height : ref.current?.offsetHeight ?? 234, - size: width ? width / 8 : 40, - color: fontColor ?? '#704234', - background: 'transparent', - backgroundAlpha: 0, - darkMode: darkMode ?? false, - fitToWidth, - minSize, - align, - }); - }, [ - initialText, - width, - height, - fontColor, - darkMode, + const { + WreathSansCanvas, + resize, + redraw, + getText, + onInputHandler, + setAlign, + getTextRect, + } = useMemo(() => { + return createWreathSans({ + initialText: initialText, + width: width ? width : ref.current?.offsetWidth ?? 330, + height: height ? height : ref.current?.offsetHeight ?? 234, + size: width ? width / 8 : 40, + color: fontColor ?? '#704234', + background: 'transparent', + backgroundAlpha: 0, + darkMode: darkMode ?? false, fitToWidth, minSize, align, - ref.current?.offsetWidth, - ref.current?.offsetHeight, - ]); + }); + }, [ + initialText, + width, + height, + fontColor, + darkMode, + fitToWidth, + minSize, + align, + ref.current?.offsetWidth, + ref.current?.offsetHeight, + ]); useEffect(() => { const handleResize = () => { @@ -74,5 +81,6 @@ export default function useWreathSans({ getText, onInputHandler, setAlign, + getTextRect, }; } From 5be86ecdf349f03dbc5d04780bc9333ce765763f Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Wed, 24 Dec 2025 00:40:12 -0500 Subject: [PATCH 4/5] fix: letter content line break --- projects/waffle-sans/src/components/ReceivedContent.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/waffle-sans/src/components/ReceivedContent.tsx b/projects/waffle-sans/src/components/ReceivedContent.tsx index 16b4cbcc..73d23f31 100644 --- a/projects/waffle-sans/src/components/ReceivedContent.tsx +++ b/projects/waffle-sans/src/components/ReceivedContent.tsx @@ -141,13 +141,14 @@ const SansWrapper = styled.div<{ $height: number | null }>` height: ${({ $height }) => ($height ? `${$height}px` : '30%')}; `; -const MainText = styled.div<{ $isOutside: boolean }>` +const MainText = styled.pre<{ $isOutside: boolean }>` color: ${({ $isOutside }) => ($isOutside ? `#315c57` : `#FEDCB4`)}; width: 100%; - height: 30%; + /* height: 30%; */ flex-shrink: 0; overflow-y: auto; + white-space: pre-wrap; text-align: justify; font-family: Inter; From 7e86299053862b0bbea47a672c24c93f32018cff Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Wed, 24 Dec 2025 02:44:17 -0500 Subject: [PATCH 5/5] refactor: dynamic letter height --- .../waffle-sans/src/components/Letter.tsx | 2 +- .../src/components/ReceivedContent.tsx | 51 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/projects/waffle-sans/src/components/Letter.tsx b/projects/waffle-sans/src/components/Letter.tsx index 9cbbef4e..b4019781 100644 --- a/projects/waffle-sans/src/components/Letter.tsx +++ b/projects/waffle-sans/src/components/Letter.tsx @@ -12,7 +12,7 @@ type LetterProps = { content: string; sans: string; mode: string; - align?: string; + align: string; }; export default function Letter({ diff --git a/projects/waffle-sans/src/components/ReceivedContent.tsx b/projects/waffle-sans/src/components/ReceivedContent.tsx index 73d23f31..a2f475a1 100644 --- a/projects/waffle-sans/src/components/ReceivedContent.tsx +++ b/projects/waffle-sans/src/components/ReceivedContent.tsx @@ -8,9 +8,9 @@ type ReceivedContentProps = { content: string; sans: string; mode: string; - stage?: string; - align?: string; - onHeightChange?: (height: number) => void; + stage: string; + align: string; + onHeightChange: (height: number) => void; }; export default function ReceivedContent({ @@ -25,7 +25,7 @@ export default function ReceivedContent({ const [width, setWidth] = useState(280); const [height, setHeight] = useState(196); const [baseHeight, setBaseHeight] = useState(null); - const [containerHeight, setContainerHeight] = useState(null); + // const [containerHeight, setContainerHeight] = useState(null); const [sansHeight, setSansHeight] = useState(null); const containerRef = useRef(null); const baseSansHeight = useMemo( @@ -77,14 +77,21 @@ export default function ReceivedContent({ return () => cancelAnimationFrame(rafId); }, [baseHeight, baseSansHeight, getTextRect, sans, stage]); + // TODO: 필요한가? + // useEffect(() => { + // if (!baseHeight || !baseSansHeight) return; + // const nextSansHeight = sansHeight ?? baseSansHeight; + // const delta = Math.max(0, nextSansHeight - baseSansHeight); + // const nextHeight = Math.ceil(baseHeight + delta); + // setContainerHeight(nextHeight); + // onHeightChange?.(nextHeight); + // }, [baseHeight, baseSansHeight, onHeightChange, sansHeight]); + useEffect(() => { - if (!baseHeight || !baseSansHeight) return; - const nextSansHeight = sansHeight ?? baseSansHeight; - const delta = Math.max(0, nextSansHeight - baseSansHeight); - const nextHeight = Math.ceil(baseHeight + delta); - setContainerHeight(nextHeight); - onHeightChange?.(nextHeight); - }, [baseHeight, baseSansHeight, onHeightChange, sansHeight]); + if (!containerRef.current) return; + onHeightChange(containerRef.current.offsetHeight); + }, [sansHeight, containerRef, onHeightChange]); + useEffect(() => { if (!sansHeight || !width) return; @@ -101,7 +108,7 @@ export default function ReceivedContent({ @@ -113,24 +120,25 @@ export default function ReceivedContent({ ); } -const Container = styled.div<{ $isOutside: boolean; $height: number | null }>` +const OUTSIDE_LETTER_BG = `${import.meta.env.BASE_URL}background_outside_letter.png`; +const INSIDE_LETTER_BG = `${import.meta.env.BASE_URL}background_inside_letter.png`; + +const Container = styled.div<{ $isOutside: boolean; $height?: number | null }>` position: relative; top: 10px; width: 100%; - height: ${({ $height }) => ($height ? `${$height}px` : 'auto')}; - aspect-ratio: ${({ $height }) => ($height ? 'auto' : '0.55')}; + /* TODO: auto로 두면 안 되는 이유? */ + /* height: ${({ $height }) => ($height ? `${$height}px` : 'auto')}; + aspect-ratio: ${({ $height }) => ($height ? 'auto' : '0.55')}; */ box-sizing: border-box; - padding: 45px 40px; + padding: 45px 40px 80px 40px; display: flex; flex-direction: column; align-items: center; - gap: 3%; + gap: 10px; - background-image: url(${({ $isOutside }) => - $isOutside - ? `${import.meta.env.BASE_URL}background_outside_letter.png` - : `${import.meta.env.BASE_URL}background_inside_letter.png`}); + background-image: url(${({ $isOutside }) => $isOutside ? OUTSIDE_LETTER_BG : INSIDE_LETTER_BG}); background-size: cover; background-position: bottom center; box-shadow: 0 6px 6px 0 rgba(0, 0, 0, 0.15); @@ -145,7 +153,6 @@ const MainText = styled.pre<{ $isOutside: boolean }>` color: ${({ $isOutside }) => ($isOutside ? `#315c57` : `#FEDCB4`)}; width: 100%; - /* height: 30%; */ flex-shrink: 0; overflow-y: auto; white-space: pre-wrap;