From a302e1143c3a3538b983a69cc462941b791f8d8d Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 00:08:20 +0900 Subject: [PATCH 01/40] =?UTF-8?q?feat=20:=20=EC=B1=84=EB=84=90=ED=86=A1=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/channel-talk/index.tsx | 197 ++++++++++++++++++++++++++ src/providers.tsx | 10 ++ 2 files changed, 207 insertions(+) create mode 100644 src/components/channel-talk/index.tsx diff --git a/src/components/channel-talk/index.tsx b/src/components/channel-talk/index.tsx new file mode 100644 index 00000000..4a6bc8d1 --- /dev/null +++ b/src/components/channel-talk/index.tsx @@ -0,0 +1,197 @@ +declare global { + interface Window { + ChannelIO?: IChannelIO; + ChannelIOInitialized?: boolean; + } +} + +interface IChannelIO { + c?: (...args: any) => void; + q?: [methodName: string, ...args: any[]][]; + (...args: any): void; +} + +interface BootOption { + appearance?: string; + customLauncherSelector?: string; + hideChannelButtonOnBoot?: boolean; + hidePopup?: boolean; + language?: string; + memberHash?: string; + memberId?: string; + pluginKey: string; + profile?: Profile; + trackDefaultEvent?: boolean; + trackUtmSource?: boolean; + unsubscribe?: boolean; + unsubscribeEmail?: boolean; + unsubscribeTexting?: boolean; + zIndex?: number; +} + +interface Callback { + (error: Error | null, user: CallbackUser | null): void; +} + +interface CallbackUser { + alert: number; + avatarUrl: string; + id: string; + language: string; + memberId: string; + name?: string; + profile?: Profile | null; + tags?: string[] | null; + unsubscribeEmail: boolean; + unsubscribeTexting: boolean; +} + +interface UpdateUserInfo { + language?: string; + profile?: Profile | null; + profileOnce?: Profile; + tags?: string[] | null; + unsubscribeEmail?: boolean; + unsubscribeTexting?: boolean; +} + +interface Profile { + [key: string]: string | number | boolean | null | undefined; +} + +interface FollowUpProfile { + name?: string | null; + mobileNumber?: string | null; + email?: string | null; +} + +interface EventProperty { + [key: string]: string | number | boolean | null | undefined; +} + +type Appearance = "light" | "dark" | "system" | null; + +class ChannelService { + loadScript() { + (function () { + var w = window; + if (w.ChannelIO) { + return w.console.error("ChannelIO script included twice."); + } + var ch: IChannelIO = function () { + ch.c?.(arguments); + }; + ch.q = []; + ch.c = function (args) { + ch.q?.push(args); + }; + w.ChannelIO = ch; + function l() { + if (w.ChannelIOInitialized) { + return; + } + w.ChannelIOInitialized = true; + var s = document.createElement("script"); + s.type = "text/javascript"; + s.async = true; + s.src = "https://cdn.channel.io/plugin/ch-plugin-web.js"; + var x = document.getElementsByTagName("script")[0]; + if (x.parentNode) { + x.parentNode.insertBefore(s, x); + } + } + if (document.readyState === "complete") { + l(); + } else { + w.addEventListener("DOMContentLoaded", l); + w.addEventListener("load", l); + } + })(); + } + + boot(option: BootOption, callback?: Callback) { + window.ChannelIO?.("boot", option, callback); + } + + shutdown() { + window.ChannelIO?.("shutdown"); + } + + showMessenger() { + window.ChannelIO?.("showMessenger"); + } + + hideMessenger() { + window.ChannelIO?.("hideMessenger"); + } + + openChat(chatId?: string | number, message?: string) { + window.ChannelIO?.("openChat", chatId, message); + } + + track(eventName: string, eventProperty?: EventProperty) { + window.ChannelIO?.("track", eventName, eventProperty); + } + + onShowMessenger(callback: () => void) { + window.ChannelIO?.("onShowMessenger", callback); + } + + onHideMessenger(callback: () => void) { + window.ChannelIO?.("onHideMessenger", callback); + } + + onBadgeChanged(callback: (unread: number, alert: number) => void) { + window.ChannelIO?.("onBadgeChanged", callback); + } + + onChatCreated(callback: () => void) { + window.ChannelIO?.("onChatCreated", callback); + } + + onFollowUpChanged(callback: (profile: FollowUpProfile) => void) { + window.ChannelIO?.("onFollowUpChanged", callback); + } + + onUrlClicked(callback: (url: string) => void) { + window.ChannelIO?.("onUrlClicked", callback); + } + + clearCallbacks() { + window.ChannelIO?.("clearCallbacks"); + } + + updateUser(userInfo: UpdateUserInfo, callback?: Callback) { + window.ChannelIO?.("updateUser", userInfo, callback); + } + + addTags(tags: string[], callback?: Callback) { + window.ChannelIO?.("addTags", tags, callback); + } + + removeTags(tags: string[], callback?: Callback) { + window.ChannelIO?.("removeTags", tags, callback); + } + + setPage(page: string) { + window.ChannelIO?.("setPage", page); + } + + resetPage() { + window.ChannelIO?.("resetPage"); + } + + showChannelButton() { + window.ChannelIO?.("showChannelButton"); + } + + hideChannelButton() { + window.ChannelIO?.("hideChannelButton"); + } + + setAppearance(appearance: Appearance) { + window.ChannelIO?.("setAppearance", appearance); + } +} + +export default new ChannelService(); diff --git a/src/providers.tsx b/src/providers.tsx index 73eeb9e9..78a0f841 100644 --- a/src/providers.tsx +++ b/src/providers.tsx @@ -6,6 +6,8 @@ import { QueryClientProvider, } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useEffect, useState } from "react"; +import ChannelService from "./components/channel-talk"; function makeQueryClient() { return new QueryClient({ @@ -31,6 +33,14 @@ function getQueryClient() { const QueryProviders = ({ children }: { children: React.ReactNode }) => { const queryClient = getQueryClient(); + useEffect(() => { + ChannelService.loadScript(); + + ChannelService.boot({ + pluginKey: "4be26d42-5350-46c1-b390-93e0d142750e", + }); + }, []); + return ( {children} From 186a38b76555e7cbab67bfada66e48ef4861a31e Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 15:42:26 +0900 Subject: [PATCH 02/40] =?UTF-8?q?refactor=20:=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=ED=86=A1,=20=ED=94=8C=EB=A1=9C=ED=8C=85=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=A0=20=EC=9D=BC=20=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B1=84=EB=84=90=ED=86=A1=20=EC=88=A8=EA=B9=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/boards/page.tsx | 4 ++-- src/app/globals.css | 22 ++++++++++++++++++++++ src/providers.tsx | 9 ++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/app/boards/page.tsx b/src/app/boards/page.tsx index 534de692..e2faa787 100644 --- a/src/app/boards/page.tsx +++ b/src/app/boards/page.tsx @@ -24,8 +24,8 @@ const Page = () => { - diff --git a/src/app/globals.css b/src/app/globals.css index 1b876629..800aa284 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -27,3 +27,25 @@ ::backdrop { background-color: rgba(0, 0, 0, 0.5); } + +#ch-plugin { + bottom: 20px !important; +} + +@media (max-width: 743px) { + #ch-plugin { + right: calc(20% + 60px) !important; + } +} + +@media (min-width: 744px) { + #ch-plugin { + right: calc(20% + 72px) !important; + } +} + +@media (min-width: 1280px) { + #ch-plugin { + right: calc(20% + 72px) !important; + } +} diff --git a/src/providers.tsx b/src/providers.tsx index 78a0f841..8c494fb4 100644 --- a/src/providers.tsx +++ b/src/providers.tsx @@ -7,6 +7,7 @@ import { } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; import ChannelService from "./components/channel-talk"; function makeQueryClient() { @@ -32,14 +33,20 @@ function getQueryClient() { const QueryProviders = ({ children }: { children: React.ReactNode }) => { const queryClient = getQueryClient(); + const pathname = usePathname(); useEffect(() => { ChannelService.loadScript(); + }, []); + + useEffect(() => { + const isTasklistPage = pathname?.includes("/tasklist"); ChannelService.boot({ pluginKey: "4be26d42-5350-46c1-b390-93e0d142750e", + hideChannelButtonOnBoot: isTasklistPage, }); - }, []); + }, [pathname]); return ( From 3d1001e20f7ee14648991e891683c9866e0f9b40 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 18:04:22 +0900 Subject: [PATCH 03/40] =?UTF-8?q?refactor=20:=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=ED=86=A1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/globals.css b/src/app/globals.css index 800aa284..4c834ba7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -30,6 +30,7 @@ #ch-plugin { bottom: 20px !important; + animation: channelFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; } @media (max-width: 743px) { From 2e959d3d08215e4d89b6eb9b9324c209b494365a Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 20:31:18 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat=20:=20=EB=B9=84=EC=86=8D=EC=96=B4=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98=20=EB=B0=8F=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80,=20=EA=B3=84=EC=A0=95=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 7 ++++ package.json | 1 + .../article-comments/article-comments.tsx | 12 ++++-- .../_components/user-setting-contents.tsx | 11 ++++-- src/utils/profanityFilter.ts | 38 +++++++++++++++++++ 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 src/utils/profanityFilter.ts diff --git a/package-lock.json b/package-lock.json index 2065998f..46c60bfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tanstack/react-query": "^5.90.5", "@tanstack/react-query-devtools": "^5.90.2", "axios": "^1.13.1", + "badwords-ko": "^1.0.4", "clsx": "^2.1.1", "framer-motion": "^12.23.24", "jotai": "^2.15.1", @@ -6400,6 +6401,12 @@ "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, + "node_modules/badwords-ko": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/badwords-ko/-/badwords-ko-1.0.4.tgz", + "integrity": "sha512-l2gJiVkIXd1nyyRI6aXEujq+aCiMfCyREUdK68mUnH6iZy8+mvXvkvOLO6Nv6MXiRKgvdUQEmSeuVno38MX+wA==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", diff --git a/package.json b/package.json index c947c29b..502435ad 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^5.90.5", "@tanstack/react-query-devtools": "^5.90.2", "axios": "^1.13.1", + "badwords-ko": "^1.0.4", "clsx": "^2.1.1", "framer-motion": "^12.23.24", "jotai": "^2.15.1", diff --git a/src/app/boards/[articleId]/_components/article-comments/article-comments.tsx b/src/app/boards/[articleId]/_components/article-comments/article-comments.tsx index fc0bf951..6c820181 100644 --- a/src/app/boards/[articleId]/_components/article-comments/article-comments.tsx +++ b/src/app/boards/[articleId]/_components/article-comments/article-comments.tsx @@ -13,6 +13,7 @@ import usePatchArticleComment from "@/hooks/api/articles/use-patch-article-comme import useDeleteArticleComment from "@/hooks/api/articles/use-delete-article-comment"; import LikeButton from "@/components/lottie/LikeButton"; import DefaultProfile from "@/assets/icons/ic-user.svg"; +import { filterProfanity } from "@/utils/profanityFilter"; interface ArticleCommentsProps { article: Article; @@ -61,10 +62,12 @@ const ArticleComments = ({ article }: ArticleCommentsProps) => { }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const handleCommentSubmit = (content: string) => { + const filteredContent = filterProfanity(content); + mutate({ articleId: article.id, data: { - content, + content: filteredContent, }, }); }; @@ -135,9 +138,10 @@ const ArticleComments = ({ article }: ArticleCommentsProps) => {
- patchComment({ commentId, content }) - } + onEdit={(commentId, content) => { + const filteredContent = filterProfanity(content); + patchComment({ commentId, content: filteredContent }); + }} onDelete={(commentId) => deleteComment({ commentId, articleId: article.id }) } diff --git a/src/app/mypage/_components/user-setting-contents.tsx b/src/app/mypage/_components/user-setting-contents.tsx index 8347389c..09f26f39 100644 --- a/src/app/mypage/_components/user-setting-contents.tsx +++ b/src/app/mypage/_components/user-setting-contents.tsx @@ -17,8 +17,11 @@ import usePatchUserPassword from "@/hooks/api/user/use-patch-user-password"; import usePatchUser from "@/hooks/api/user/use-patch-user"; import { useGetUserInfoQuery } from "@/hooks/api/user/use-get-user-info-query"; import isSocialLogin from "@/utils/auth-helper"; +import { hasProfanity } from "@/utils/profanityFilter"; +import useToast from "@/hooks/use-toast"; const UserSettingContents = () => { + const toast = useToast(); const { Modal: DeleteModal, openPrompt: openDeleteModal, @@ -59,13 +62,13 @@ const UserSettingContents = () => { profileImage !== (userInfo?.image || ""); const handleSaveChanges = () => { - if (!nickname.trim()) { - return; - } - const updates: { nickname?: string; image?: string } = {}; if (nickname !== (userInfo?.nickname || "")) { + if (hasProfanity(nickname)) { + toast.error("부적절한 닉네임입니다. 닉네임을 변경해주세요."); + return; + } updates.nickname = nickname; } diff --git a/src/utils/profanityFilter.ts b/src/utils/profanityFilter.ts new file mode 100644 index 00000000..9a7c1e2a --- /dev/null +++ b/src/utils/profanityFilter.ts @@ -0,0 +1,38 @@ +// @ts-ignore - badwords-ko has no type definitions +import Filter from "badwords-ko"; + +const filter = new Filter(); + +/** + * 텍스트에서 욕설을 *로 치환하는 함수 + * @param text 필터링할 텍스트 + * @returns 욕설이 *로 치환된 텍스트 + */ +export const filterProfanity = (text: string): string => { + return filter.clean(text); +}; + +/** + * 텍스트에 욕설이 포함되어 있는지 확인하는 함수 + * @param text 확인할 텍스트 + * @returns 욕설 포함 여부 + */ +export const hasProfanity = (text: string): boolean => { + return filter.isProfane(text); +}; + +/** + * 사용자 정의 욕설을 추가하는 함수 + * @param words 추가할 욕설 단어 배열 + */ +export const addBadWords = (words: string[]): void => { + filter.addWords(...words); +}; + +/** + * 욕설 목록에서 단어를 제거하는 함수 + * @param words 제거할 단어 배열 + */ +export const removeBadWords = (words: string[]): void => { + filter.removeWords(...words); +}; From 58ef6a70ff97289711f1574eb2df2968aa67f2d7 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 20:39:51 +0900 Subject: [PATCH 05/40] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1,=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EC=86=8D=EC=96=B4=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article-edit-contents/article-edit-contents.tsx | 8 ++++++-- .../article-write-contents/article-write-contents.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/boards/[articleId]/edit/_components/article-edit-contents/article-edit-contents.tsx b/src/app/boards/[articleId]/edit/_components/article-edit-contents/article-edit-contents.tsx index f9fc0d23..db5f54ea 100644 --- a/src/app/boards/[articleId]/edit/_components/article-edit-contents/article-edit-contents.tsx +++ b/src/app/boards/[articleId]/edit/_components/article-edit-contents/article-edit-contents.tsx @@ -12,6 +12,7 @@ import { } from "@/components/index"; import usePatchArticle from "@/hooks/api/articles/use-patch-article"; import { Article } from "@/types/article"; +import { filterProfanity } from "@/utils/profanityFilter"; interface ArticleEditContentsProps { article: Article; @@ -44,12 +45,15 @@ const ArticleEditContents = ({ article }: ArticleEditContentsProps) => { }, []); const onSubmit = (data: EditFormData) => { + const filteredTitle = filterProfanity(data.title); + const filteredContent = filterProfanity(data.content); + mutate( { articleId: article.id, data: { - title: data.title, - content: data.content, + title: filteredTitle, + content: filteredContent, image: images[0] || undefined, }, }, diff --git a/src/app/boards/write/_components/article-write-contents/article-write-contents.tsx b/src/app/boards/write/_components/article-write-contents/article-write-contents.tsx index 474459e3..48387be3 100644 --- a/src/app/boards/write/_components/article-write-contents/article-write-contents.tsx +++ b/src/app/boards/write/_components/article-write-contents/article-write-contents.tsx @@ -11,6 +11,7 @@ import { LoadingSpinner, } from "@/components/index"; import { usePostArticle } from "@/hooks/api/articles/use-post-article"; +import { filterProfanity } from "@/utils/profanityFilter"; interface WriteFormData { title: string; @@ -32,10 +33,13 @@ const ArticleWriteContents = () => { }, []); const onSubmit = (data: WriteFormData) => { + const filteredTitle = filterProfanity(data.title); + const filteredContent = filterProfanity(data.content); + mutate( { - title: data.title, - content: data.content, + title: filteredTitle, + content: filteredContent, image: images[0] || undefined, }, { From c554bb31b4c64b90a285a82a9bd1700a90dacd98 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 20:51:08 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EB=B9=84?= =?UTF-8?q?=EC=86=8D=EC=96=B4=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/signup/page.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 342cc978..c5adb529 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import SingUpInFormWrapper from "../_components/form_wrapper"; import { useForm } from "react-hook-form"; import { @@ -12,6 +12,7 @@ import { Button, Icon, TextInput, LoadingSpinner } from "@/components"; import { useSignupQuery } from "@/hooks/auth/use-signup-query"; import type { SignupRequest } from "@/api/auth/signup-action"; import SimpleSignUpIn from "../_components/simple-signUpIn"; +import { hasProfanity } from "@/utils/profanityFilter"; const Page = () => { const [showPassword, setShowPassword] = useState(false); @@ -57,6 +58,10 @@ const Page = () => { aria-describedby={errors.nickname ? "nickname-error" : undefined} {...register("nickname", { required: "닉네임을 입력해주세요.", + validate: { + noProfanity: (value) => + !hasProfanity(value) || "부적절한 닉네임입니다.", + }, })} /> From b586d84ab3d04da1c26d83152aba41d7b81326e8 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 21:05:08 +0900 Subject: [PATCH 07/40] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B9=84?= =?UTF-8?q?=EC=86=8D=EC=96=B4=20=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=8F=20=EC=B6=94=EA=B0=80=20=EB=B9=84=EC=86=8D?= =?UTF-8?q?=EC=96=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/signup/page.tsx | 9 ++++++++ src/constants/custom-profanity.ts | 34 +++++++++++++++++++++++++++++++ src/utils/profanityFilter.ts | 5 +++++ 3 files changed, 48 insertions(+) create mode 100644 src/constants/custom-profanity.ts diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index c5adb529..287d1b06 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -80,6 +80,15 @@ const Page = () => { value: EMAIL_REGEX, message: "이메일 형식이 올바르지 않습니다.", }, + validate: { + noProfanityInEmail: (value) => { + const localPart = value.split("@")[0]; + return ( + !hasProfanity(localPart) || + "이메일에 부적절한 단어가 포함되어 있습니다." + ); + }, + }, })} /> diff --git a/src/constants/custom-profanity.ts b/src/constants/custom-profanity.ts new file mode 100644 index 00000000..54b5f44d --- /dev/null +++ b/src/constants/custom-profanity.ts @@ -0,0 +1,34 @@ +/** + * 커스텀 비속어 목록 + * 영어 표기 한글 비속어 등 추가로 필터링할 단어들을 관리 + */ +export const customProfanityWords: string[] = [ + "sibal", + "shibal", + "sival", + "shival", + "ssibal", + "ssival", + "seki", + "gae", + "gaeseki", + "gaesekki", + "gaesekiya", + "gsk", + "gesseki", + "geseki", + "jot", + "johd", + "jott", + "fuck", + "shit", + "fk", + "fuk", + "fuc", + "dick", + "bitch", + "damn", + "rotoRL", + "qudtls", + "whssk", +]; diff --git a/src/utils/profanityFilter.ts b/src/utils/profanityFilter.ts index 9a7c1e2a..17317f4a 100644 --- a/src/utils/profanityFilter.ts +++ b/src/utils/profanityFilter.ts @@ -1,8 +1,13 @@ // @ts-ignore - badwords-ko has no type definitions import Filter from "badwords-ko"; +import { customProfanityWords } from "@/constants/custom-profanity"; const filter = new Filter(); +if (customProfanityWords.length > 0) { + filter.addWords(...customProfanityWords); +} + /** * 텍스트에서 욕설을 *로 치환하는 함수 * @param text 필터링할 텍스트 From 00a88b118ae95c101b443446a32672e0ff0f079d Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 21:16:14 +0900 Subject: [PATCH 08/40] =?UTF-8?q?fix=20:=20calendar-time=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20=ED=8C=8C=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/calendar-time/calendar-time.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/calendar-time/calendar-time.tsx b/src/components/calendar-time/calendar-time.tsx index 959eedd9..ca0342b2 100644 --- a/src/components/calendar-time/calendar-time.tsx +++ b/src/components/calendar-time/calendar-time.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button } from "@/components/index"; +import Button from "../button/button"; import { TIME_LIST } from "@/constants/time-list"; import cn from "@/utils/clsx"; import { useEffect, useState } from "react"; From c6db597bf1ecc0f87573369eef8a4a08b4fa64ed Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 21:22:53 +0900 Subject: [PATCH 09/40] =?UTF-8?q?fix=20:=20Reply=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/reply/reply.stories.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/reply/reply.stories.tsx b/src/components/reply/reply.stories.tsx index 2c9540a8..5a6753c5 100644 --- a/src/components/reply/reply.stories.tsx +++ b/src/components/reply/reply.stories.tsx @@ -1,5 +1,14 @@ import { Meta, StoryObj } from "@storybook/nextjs"; import Reply from "./reply"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); const meta = { title: "Components/Reply", @@ -21,9 +30,11 @@ const meta = { }, decorators: [ (Story) => ( -
- -
+ +
+ +
+
), ], } satisfies Meta; From e1c5a7799d4c4680b2d4acddf6bfa12615e67aa3 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 21:26:40 +0900 Subject: [PATCH 10/40] =?UTF-8?q?fix=20:=20task-card=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/task-card/task-card.stories.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/task-card/task-card.stories.tsx b/src/components/task-card/task-card.stories.tsx index f7f51500..7a5476a9 100644 --- a/src/components/task-card/task-card.stories.tsx +++ b/src/components/task-card/task-card.stories.tsx @@ -1,5 +1,14 @@ import type { Meta, StoryObj } from "@storybook/react"; import TaskCard from "./task-card"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); const meta = { title: "Components/TaskCard", @@ -14,6 +23,13 @@ const meta = { completed: { control: "number" }, taskList: { control: "object" }, }, + decorators: [ + (Story) => ( + + + + ), + ], } satisfies Meta; export default meta; From 5202f5f1c9769e10ed7d49822bb2c70a74b3e9e9 Mon Sep 17 00:00:00 2001 From: junye0l Date: Fri, 28 Nov 2025 21:28:25 +0900 Subject: [PATCH 11/40] =?UTF-8?q?fix=20:=20calendar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20?= =?UTF-8?q?=EC=9B=B9=ED=8C=A9=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/calendar/calendar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/calendar/calendar.tsx b/src/components/calendar/calendar.tsx index 99637bed..93427a34 100644 --- a/src/components/calendar/calendar.tsx +++ b/src/components/calendar/calendar.tsx @@ -1,6 +1,7 @@ "use client"; -import { Button, Icon } from "@/components/index"; +import Button from "../button/button"; +import Icon from "../icon/Icon"; import cn from "@/utils/clsx"; import type { CSSProperties } from "react"; import { useState } from "react"; From 7bae24418cf2bd7660831c8f1f48f80e8f7d3209 Mon Sep 17 00:00:00 2001 From: JinHyuk Kim Date: Fri, 28 Nov 2025 15:54:18 +0900 Subject: [PATCH 12/40] =?UTF-8?q?feat:=20=ED=8C=80=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[groupId]/page.tsx | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/[groupId]/page.tsx b/src/app/[groupId]/page.tsx index de9166a8..ba92e4ff 100644 --- a/src/app/[groupId]/page.tsx +++ b/src/app/[groupId]/page.tsx @@ -1,3 +1,5 @@ +import { Metadata } from "next"; +import { cookies } from "next/headers"; import TeamPageClient from "./_components/team-page-client"; interface TeamPageProps { @@ -6,6 +8,55 @@ interface TeamPageProps { }>; } +async function getAccessToken() { + const cookieStore = await cookies(); + return cookieStore.get("accessToken")?.value; +} + +export async function generateMetadata({ + params, +}: TeamPageProps): Promise { + const { groupId } = await params; + const accessToken = await getAccessToken(); + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/groups/${groupId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + const group = await res.json(); + if (!group) { + return { + title: "팀페이지", + description: "팀 페이지 정보를 확인할 수 있습니다.", + }; + } + + return { + title: group.name, + description: `${group.name} 정보를 확인할 수 있습니다.`, + openGraph: { + title: `${group.name} | Coworkers`, + description: `${group.name} 정보를 확인할 수 있습니다.`, + type: "website", + url: `https://coworkers-pied.vercel.app/${groupId}`, + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "Coworkers", + }, + ], + }, + }; +} + const TeamPage = async ({ params }: TeamPageProps) => { const { groupId } = await params; From cf7c76db92cfe87155b7152a1afb5393a88830ca Mon Sep 17 00:00:00 2001 From: JinHyuk Kim Date: Fri, 28 Nov 2025 15:54:37 +0900 Subject: [PATCH 13/40] =?UTF-8?q?feat:=20=ED=8C=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[groupId]/editteam/page.tsx | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/[groupId]/editteam/page.tsx b/src/app/[groupId]/editteam/page.tsx index a996cf24..19e335a4 100644 --- a/src/app/[groupId]/editteam/page.tsx +++ b/src/app/[groupId]/editteam/page.tsx @@ -1,3 +1,5 @@ +import { Metadata } from "next"; +import { cookies } from "next/headers"; import EditTeam from "./_components/edit-team"; interface EditTeamPageProps { @@ -6,6 +8,55 @@ interface EditTeamPageProps { }>; } +async function getAccessToken() { + const cookieStore = await cookies(); + return cookieStore.get("accessToken")?.value; +} + +export async function generateMetadata({ + params, +}: EditTeamPageProps): Promise { + const { groupId } = await params; + const accessToken = await getAccessToken(); + + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/groups/${groupId}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + const group = await res.json(); + if (!group) { + return { + title: "팀 설정", + description: "팀 정보를 수정할 수 있습니다.", + }; + } + + return { + title: `${group.name} 팀 설정`, + description: `${group.name} 정보를 수정할 수 있습니다.`, + openGraph: { + title: `${group.name} 팀 설정 | Coworkers`, + description: `${group.name} 정보를 수정할 수 있습니다.`, + type: "website", + url: `https://coworkers-pied.vercel.app/${groupId}/editteam`, + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "Coworkers", + }, + ], + }, + }; +} + const EditTeamPage = async ({ params }: EditTeamPageProps) => { const { groupId } = await params; From c73476bc4353bf2823860acd15ec3df4fcb6c4f3 Mon Sep 17 00:00:00 2001 From: JinHyuk Kim Date: Fri, 28 Nov 2025 15:54:49 +0900 Subject: [PATCH 14/40] =?UTF-8?q?feat:=20=ED=8C=80=20=EC=97=86=EC=9D=8C=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/noteam/page.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/app/noteam/page.tsx b/src/app/noteam/page.tsx index 45c342dd..3aa462ab 100644 --- a/src/app/noteam/page.tsx +++ b/src/app/noteam/page.tsx @@ -1,8 +1,30 @@ import { Button } from "@/components/index"; import cn from "@/utils/clsx"; +import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; +export const metadata: Metadata = { + title: "팀 없음", + description: "아직 소속된 팀이 없습니다.", + openGraph: { + title: "팀 없음 | Coworkers", + description: "아직 소속된 팀이 없습니다.", + type: "website", + url: "https://coworkes.com/mypage", + locale: "ko_KR", + siteName: "Coworkers", + images: [ + { + url: "https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Coworkers/user/2449/open_graph.jpg", + width: 1200, + height: 630, + alt: "소속팀 없음", + }, + ], + }, +}; + const NoTeam = () => { return (
Date: Fri, 28 Nov 2025 16:04:29 +0900 Subject: [PATCH 15/40] =?UTF-8?q?docs:=20README=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B8=B0=EC=88=A0?= =?UTF-8?q?=20=EC=8A=A4=ED=83=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 04f04af8..88c56faf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # 💎 Coworkers 💎 -랜딩페이지 이미지 (추가 예정) +![open_graph](https://github.com/user-attachments/assets/8b7f79ce-1b21-46a6-bdff-5f4887d5ae11) +

@@ -52,11 +53,17 @@ ### 🧩 프론트엔드 -### 🛠 코드 포매터 및 검사 도구 - + +### 🛠 코드 포매터 및 도구 + + + ### 🤝 협업 도구 + + ### 🚀 배포 플랫폼 +
From 8a3f8ffcd53d906bb919a8c65e4af125cb33adae Mon Sep 17 00:00:00 2001 From: junye0l Date: Sat, 29 Nov 2025 14:32:36 +0900 Subject: [PATCH 16/40] =?UTF-8?q?refactor=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1,=20=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EC=B5=9C=EB=8C=80=20=EA=B8=80=EC=9E=90=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C=20=EB=B0=8F=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article-header/article-header.tsx | 4 ++- src/components/input-reply/input-reply.tsx | 20 ++++++++++-- src/components/reply/reply.tsx | 32 +++++++++++++++---- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/app/boards/[articleId]/_components/article-header/article-header.tsx b/src/app/boards/[articleId]/_components/article-header/article-header.tsx index e46485cf..0e2095c7 100644 --- a/src/app/boards/[articleId]/_components/article-header/article-header.tsx +++ b/src/app/boards/[articleId]/_components/article-header/article-header.tsx @@ -46,7 +46,9 @@ const ArticleHeader = ({ article }: ArticleHeaderProps) => { <>

-

{article.title}

+

+ {article.title} +

{isWriter && ( { const [value, setValue] = useState(""); + const MAX_LENGTH = 255; + const handleChange = (e: React.ChangeEvent) => { - setValue(e.target.value); + if (e.target.value.length <= MAX_LENGTH) { + setValue(e.target.value); + } }; const handleSubmit = () => { @@ -43,6 +46,7 @@ const InputReply = ({ value={value} onChange={handleChange} disabled={disabled} + maxLength={MAX_LENGTH} className="w-full max-w-[708px] resize-none text-xs text-blue-700 placeholder:text-gray-800 focus:outline-none tablet:text-md" minRows={1} /> @@ -58,6 +62,18 @@ const InputReply = ({
+ {value.length > 0 && ( +
+ + {value.length}/{MAX_LENGTH} + +
+ )}
); }; diff --git a/src/components/reply/reply.tsx b/src/components/reply/reply.tsx index d83f636f..8ce56154 100644 --- a/src/components/reply/reply.tsx +++ b/src/components/reply/reply.tsx @@ -21,6 +21,7 @@ const Reply = ({ comment, onEdit, onDelete }: CommentProps) => { const { data: userInfo } = useGetUserInfoQuery(); const [isEditing, setIsEditing] = useState(false); const [editedContent, setEditedContent] = useState(comment.content); + const MAX_LENGTH = 255; const handleEditClick = () => { setIsEditing(true); @@ -95,12 +96,29 @@ const Reply = ({ comment, onEdit, onDelete }: CommentProps) => { {isEditing ? (
- setEditedContent(e.target.value)} - className="w-full resize-none rounded-lg border border-blue-400 px-2 py-2 text-md leading-relaxed focus:outline-none" - minRows={3} - /> +
+ { + if (e.target.value.length <= MAX_LENGTH) { + setEditedContent(e.target.value); + } + }} + maxLength={MAX_LENGTH} + className="w-full resize-none rounded-lg border border-blue-400 px-2 pb-6 pt-2 text-md leading-relaxed focus:outline-none" + minRows={3} + /> + + {editedContent.length}/{MAX_LENGTH} + +
) : ( -

+

{editedContent}

)} From 7cdba7cde2165a3820072f0959300e4381eebbc8 Mon Sep 17 00:00:00 2001 From: junye0l Date: Sat, 29 Nov 2025 14:37:17 +0900 Subject: [PATCH 17/40] =?UTF-8?q?chore=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EB=A7=A4=EC=A7=81=EB=84=98=EB=B2=84=20?= =?UTF-8?q?=EB=B3=84=EB=8F=84=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/input-reply/input-reply.tsx | 12 +++++++----- src/components/reply/reply.tsx | 10 +++++----- src/constants/comment.ts | 6 ++++++ 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/constants/comment.ts diff --git a/src/components/input-reply/input-reply.tsx b/src/components/input-reply/input-reply.tsx index 0509ed8c..614a795c 100644 --- a/src/components/input-reply/input-reply.tsx +++ b/src/components/input-reply/input-reply.tsx @@ -5,6 +5,7 @@ import cn from "@/utils/clsx"; import Button from "../button/button"; import Icon from "../icon/Icon"; import TextareaAutosize from "react-textarea-autosize"; +import { MAX_COMMENT_LENGTH } from "@/constants/comment"; /** * @author junyeol @@ -23,10 +24,9 @@ const InputReply = ({ disabled = false, }: InputReplyProps) => { const [value, setValue] = useState(""); - const MAX_LENGTH = 255; const handleChange = (e: React.ChangeEvent) => { - if (e.target.value.length <= MAX_LENGTH) { + if (e.target.value.length <= MAX_COMMENT_LENGTH) { setValue(e.target.value); } }; @@ -46,7 +46,7 @@ const InputReply = ({ value={value} onChange={handleChange} disabled={disabled} - maxLength={MAX_LENGTH} + maxLength={MAX_COMMENT_LENGTH} className="w-full max-w-[708px] resize-none text-xs text-blue-700 placeholder:text-gray-800 focus:outline-none tablet:text-md" minRows={1} /> @@ -67,10 +67,12 @@ const InputReply = ({ - {value.length}/{MAX_LENGTH} + {value.length}/{MAX_COMMENT_LENGTH} )} diff --git a/src/components/reply/reply.tsx b/src/components/reply/reply.tsx index 8ce56154..898cccb6 100644 --- a/src/components/reply/reply.tsx +++ b/src/components/reply/reply.tsx @@ -10,6 +10,7 @@ import TextareaAutosize from "react-textarea-autosize"; import DefaultProfile from "@/assets/icons/ic-user.svg"; import { toDotDateString } from "@/utils/date-util"; import { useGetUserInfoQuery } from "@/hooks/api/user/use-get-user-info-query"; +import { MAX_COMMENT_LENGTH } from "@/constants/comment"; interface CommentProps { comment: Comment; @@ -21,7 +22,6 @@ const Reply = ({ comment, onEdit, onDelete }: CommentProps) => { const { data: userInfo } = useGetUserInfoQuery(); const [isEditing, setIsEditing] = useState(false); const [editedContent, setEditedContent] = useState(comment.content); - const MAX_LENGTH = 255; const handleEditClick = () => { setIsEditing(true); @@ -100,23 +100,23 @@ const Reply = ({ comment, onEdit, onDelete }: CommentProps) => { { - if (e.target.value.length <= MAX_LENGTH) { + if (e.target.value.length <= MAX_COMMENT_LENGTH) { setEditedContent(e.target.value); } }} - maxLength={MAX_LENGTH} + maxLength={MAX_COMMENT_LENGTH} className="w-full resize-none rounded-lg border border-blue-400 px-2 pb-6 pt-2 text-md leading-relaxed focus:outline-none" minRows={3} /> - {editedContent.length}/{MAX_LENGTH} + {editedContent.length}/{MAX_COMMENT_LENGTH}
diff --git a/src/constants/comment.ts b/src/constants/comment.ts new file mode 100644 index 00000000..7edf56d2 --- /dev/null +++ b/src/constants/comment.ts @@ -0,0 +1,6 @@ +/** + * 댓글 관련 상수 + */ + +/** 댓글 내용 최대 길이 */ +export const MAX_COMMENT_LENGTH = 255; From 3aed910adc8a2e2f9c49bedb9e58f432cd7c17da Mon Sep 17 00:00:00 2001 From: junye0l Date: Sat, 29 Nov 2025 14:45:49 +0900 Subject: [PATCH 18/40] =?UTF-8?q?refactor=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B2=84=ED=8A=BC=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/input-reply/input-reply.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/input-reply/input-reply.tsx b/src/components/input-reply/input-reply.tsx index 614a795c..d6d7a7fe 100644 --- a/src/components/input-reply/input-reply.tsx +++ b/src/components/input-reply/input-reply.tsx @@ -40,7 +40,7 @@ const InputReply = ({ return (
-
+
Date: Sat, 29 Nov 2025 15:03:43 +0900 Subject: [PATCH 19/40] =?UTF-8?q?refactor=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/input-reply/input-reply.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/input-reply/input-reply.tsx b/src/components/input-reply/input-reply.tsx index d6d7a7fe..2c40af25 100644 --- a/src/components/input-reply/input-reply.tsx +++ b/src/components/input-reply/input-reply.tsx @@ -40,14 +40,14 @@ const InputReply = ({ return (
-
+