From cad48bd5d51abb72268f519f5cb4ed482fb6f3bb Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:37:48 +0900 Subject: [PATCH 01/19] =?UTF-8?q?chore:=20=EC=95=88=EC=93=B0=EB=8A=94=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team/[teamId]/_components/MemberWidget/MemberWidget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(route)/team/[teamId]/_components/MemberWidget/MemberWidget.tsx b/src/app/(route)/team/[teamId]/_components/MemberWidget/MemberWidget.tsx index ea0d7ab2..44d70fd7 100644 --- a/src/app/(route)/team/[teamId]/_components/MemberWidget/MemberWidget.tsx +++ b/src/app/(route)/team/[teamId]/_components/MemberWidget/MemberWidget.tsx @@ -1,7 +1,7 @@ "use client"; -import { useGetGroups, useGetInvitation } from "@/api/hooks"; -import { BaseButton, FloatingButton, Modal } from "@/common"; +import { useGetGroups } from "@/api/hooks"; +import { FloatingButton } from "@/common"; import { useParams } from "next/navigation"; import { useState } from "react"; import WidgetProfile from "./_internal/WidgetProfile"; From 7a44ccd2b99a8caa9a2a4a03aa5d5e462f02cdf9 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:46:29 +0900 Subject: [PATCH 02/19] =?UTF-8?q?fix:=20=EC=B9=B4=EB=93=9C=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=ED=85=9C=20=EB=86=92=EC=9D=B4=20=EB=B0=8F=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EB=A7=9E=EC=B6=94=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(route)/dashboard/_components/Article/BestArticleCard.tsx | 3 ++- .../_components/Article/_internal/ArticleContent.tsx | 2 +- .../Section/DashBoardBestArticles/DashBoardBestArticles.tsx | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx index 9ceff105..85442ea3 100644 --- a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx +++ b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx @@ -17,8 +17,9 @@ const BestArticleCard = ({ articleId }: { articleId: number }) => { return ( -
+
+
diff --git a/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx b/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx index 5ed82541..7e119615 100644 --- a/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx +++ b/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx @@ -1,5 +1,5 @@ const ArticleContent = ({ content }: { content: string }) => { - return

{content}

; + return

{content}

; }; export default ArticleContent; diff --git a/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx b/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx index c746002d..775e88ea 100644 --- a/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx +++ b/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx @@ -23,9 +23,9 @@ const DashBoardBestArticles = () => {

베스트 게시글

-
    +
      {articles?.list.map((article) => ( -
    • +
    • ))} From 2302e0525fa3efcad9ca71c085affa334537a7b2 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:04:26 +0900 Subject: [PATCH 03/19] =?UTF-8?q?refactor:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EC=85=98=20endPage=EB=A5=BC=20=EB=84=98?= =?UTF-8?q?=EC=96=B4=EA=B0=88=20=EB=95=8C=20=ED=98=84=EC=9E=AC=20page?= =?UTF-8?q?=EB=A5=BC=20=EB=A7=A8=20=EC=95=9E=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Section/DashBoardBestArticles/DashBoardBestArticles.tsx | 2 +- .../Section/DashBoardBestArticles/_internal/Pagination.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx b/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx index 775e88ea..cf65df41 100644 --- a/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx +++ b/src/app/dashboard/_components/Section/DashBoardBestArticles/DashBoardBestArticles.tsx @@ -23,7 +23,7 @@ const DashBoardBestArticles = () => {

      베스트 게시글

      -
        +
          {articles?.list.map((article) => (
        • diff --git a/src/app/dashboard/_components/Section/DashBoardBestArticles/_internal/Pagination.tsx b/src/app/dashboard/_components/Section/DashBoardBestArticles/_internal/Pagination.tsx index ecf37808..744efde9 100644 --- a/src/app/dashboard/_components/Section/DashBoardBestArticles/_internal/Pagination.tsx +++ b/src/app/dashboard/_components/Section/DashBoardBestArticles/_internal/Pagination.tsx @@ -10,8 +10,8 @@ interface PaginationProps { } const Pagination = ({ page, totalPages, onPrev, onNext, maxDots = 5 }: PaginationProps) => { - const startPage = Math.max(1, page - Math.floor(maxDots / 2)); - const endPage = Math.min(totalPages, startPage + maxDots - 1); + const startPage = Math.floor((page - 1) / maxDots) * maxDots + 1; + const endPage = Math.min(startPage + maxDots - 1, totalPages); const visiblePages = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i); From b71e23ab04dcdcc4682f9c2527650e6bb23522c2 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:37:29 +0900 Subject: [PATCH 04/19] =?UTF-8?q?refactor:=20formatClampledCount=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/_components/Article/_internal/ArticleLike.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(route)/dashboard/_components/Article/_internal/ArticleLike.tsx b/src/app/(route)/dashboard/_components/Article/_internal/ArticleLike.tsx index ba20daeb..764df19d 100644 --- a/src/app/(route)/dashboard/_components/Article/_internal/ArticleLike.tsx +++ b/src/app/(route)/dashboard/_components/Article/_internal/ArticleLike.tsx @@ -1,5 +1,7 @@ +import { formatClampedCount } from "@/utils"; + const ArticleLike = ({ likeCount }: { likeCount: number }) => { - const count = likeCount > 999 ? "999+" : String(likeCount); + const count = formatClampedCount(likeCount, 999); return {count}; }; From 0b7eea676f75747c50e9af2deb4ffb39b07ce045 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:34:51 +0900 Subject: [PATCH 05/19] =?UTF-8?q?refactor:=20Modal,=20Dropdown=20CreatePor?= =?UTF-8?q?atl=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_internal/TaskCard/TaskCard.tsx | 4 +- src/app/layout.tsx | 1 + src/common/Dropdown/Dropdown.tsx | 97 +++++++++---------- src/common/Modal/Modal.tsx | 9 +- src/common/Portal/Portal.tsx | 17 ++++ src/common/Select/Select.tsx | 2 +- src/common/index.ts | 1 + src/utils/getPositionByPlacement.ts | 17 ++++ src/utils/index.ts | 1 + 9 files changed, 93 insertions(+), 56 deletions(-) create mode 100644 src/common/Portal/Portal.tsx create mode 100644 src/utils/getPositionByPlacement.ts diff --git a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskCard/TaskCard.tsx b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskCard/TaskCard.tsx index 906b64a2..0b4fa3dd 100644 --- a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskCard/TaskCard.tsx +++ b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskCard/TaskCard.tsx @@ -23,10 +23,12 @@ const TaskHeader = ({ name, taskList }: { name: string; taskList: TaskList }) => {name} -
          + +
          setIsOpenEditModal(true) }, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b737ca49..1fdfba09 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,6 +33,7 @@ export default function RootLayout({
          {children}
          +
          ); diff --git a/src/common/Dropdown/Dropdown.tsx b/src/common/Dropdown/Dropdown.tsx index b3101254..3c30b1c7 100644 --- a/src/common/Dropdown/Dropdown.tsx +++ b/src/common/Dropdown/Dropdown.tsx @@ -5,32 +5,14 @@ import { MouseEvent, ReactNode, useRef, useState } from "react"; import { cn } from "@/utils"; import Icon, { IconKeys } from "../Icon/Icon"; import { DropdownOption } from "./_types/types"; +import Portal from "../Portal/Portal"; +import { DropdownPlacement, getPositionByPlacement } from "@/utils/getPositionByPlacement"; -/** - * @author sangin - * - * @example - * ```tsx - * const options = [ - * { label: "마이 히스토리", action: ()=>{} }, - * { label: "로그아웃", action: ()=>{} }, - * ]; - * - * - * ``` - */ - -type DropdownPlacement = "bottom-left" | "bottom-right" | "top-left" | "top-right"; - -const PLACEMENT_BY_STYLE = { - "bottom-left": "left-0 top-full", - "bottom-right": "right-0 top-full", - "top-left": "left-0 bottom-full", - "top-right": "right-0 bottom-full", +const PLACEMENT_TRANSFORM: Record = { + "bottom-left": "translate-x-0 translate-y-0", + "top-left": "translate-x-0 -translate-y-full", + "bottom-right": "-translate-x-full translate-y-0", + "top-right": "-translate-x-full -translate-y-full", }; interface DropdownProps { @@ -51,12 +33,19 @@ const Dropdown = ({ placement = "bottom-left", }: DropdownProps) => { const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); + const [position, setPosition] = useState({ top: 0, left: 0 }); - const placementStyle = PLACEMENT_BY_STYLE[placement]; + const triggerRef = useRef(null); + const dropdownRef = useRef(null); const handleDropdownClick = (e: MouseEvent) => { e.stopPropagation(); + if (!triggerRef.current) return; + + const rect = triggerRef.current.getBoundingClientRect(); + const pos = getPositionByPlacement(rect, placement); + + setPosition(pos); setIsOpen((prev) => !prev); }; @@ -66,35 +55,41 @@ const Dropdown = ({ }; useDropdownClose(dropdownRef, () => setIsOpen(false), isOpen); + return ( -
          - {isOpen && ( -
            - {options.map((option) => ( -
          • - -
          • - ))} -
          + +
            + {options.map((option) => ( +
          • + +
          • + ))} +
          +
          )} -
          + ); }; diff --git a/src/common/Modal/Modal.tsx b/src/common/Modal/Modal.tsx index ec90615d..96391636 100644 --- a/src/common/Modal/Modal.tsx +++ b/src/common/Modal/Modal.tsx @@ -11,6 +11,7 @@ import { } from "./MODAL_STYLE"; import Icon from "../Icon/Icon"; import { ModalProps, ModalContentProps } from "./_types/ModalProps"; +import Portal from "../Portal/Portal"; /** * @author sangin @@ -34,9 +35,11 @@ const Modal = ({ isOpen, onClose, className, children }: ModalProps) => { if (!isOpen) return null; return ( -
          -
          {children}
          -
          + +
          +
          {children}
          +
          +
          ); }; diff --git a/src/common/Portal/Portal.tsx b/src/common/Portal/Portal.tsx new file mode 100644 index 00000000..1eec64c6 --- /dev/null +++ b/src/common/Portal/Portal.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { ReactNode, useState } from "react"; +import { createPortal } from "react-dom"; + +const Portal = ({ children }: { children: ReactNode }) => { + const [mounted] = useState(() => typeof window !== "undefined"); + + if (!mounted) return null; + + const root = document.getElementById("portal-root"); + if (!root) return null; + + return createPortal(children, root); +}; + +export default Portal; diff --git a/src/common/Select/Select.tsx b/src/common/Select/Select.tsx index 47778b84..b1bcab10 100644 --- a/src/common/Select/Select.tsx +++ b/src/common/Select/Select.tsx @@ -67,7 +67,7 @@ const Select = ({ value, options, onChange, className, textAlign = "left" }: {isOpen && ( -
            +
              {options.map((option) => (
          From 90292ce9305592336ef65da5536dc282d1483cc4 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:01:16 +0900 Subject: [PATCH 07/19] =?UTF-8?q?refactor:=20=EA=B8=80=EC=93=B0=EA=B8=B0?= =?UTF-8?q?=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(route)/dashboard/write/_components/ArticleForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(route)/dashboard/write/_components/ArticleForm.tsx b/src/app/(route)/dashboard/write/_components/ArticleForm.tsx index f5662ed5..c2d67fd0 100644 --- a/src/app/(route)/dashboard/write/_components/ArticleForm.tsx +++ b/src/app/(route)/dashboard/write/_components/ArticleForm.tsx @@ -24,6 +24,8 @@ const ArticleForm = () => { image: null, }); + const isSubmitDisabled = isPending || !formState.title.trim() || !formState.content.trim(); + const { error } = toastKit(); const handleTextChange = (e: ChangeEvent) => { @@ -123,7 +125,7 @@ const ArticleForm = () => { )}
          - + {isPending ? "등록 중..." : "등록하기"} From 74e29e082c5e2943d515cb6ddd6a08c48038c182 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 01:09:05 +0900 Subject: [PATCH 08/19] =?UTF-8?q?refactor:=20=ED=95=A0=20=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EB=B0=8F=20=EB=AA=A8=EB=8B=AC=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/TaskSection/TaskSection.tsx | 2 +- .../_internal/Modal/CreateTaskListModal.tsx | 51 +++++++++++++++++++ .../_internal/TaskColumn/TaskColumn.tsx | 39 ++------------ 3 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/CreateTaskListModal.tsx diff --git a/src/app/(route)/team/[teamId]/_components/TaskSection/TaskSection.tsx b/src/app/(route)/team/[teamId]/_components/TaskSection/TaskSection.tsx index db4ba74e..b723051f 100644 --- a/src/app/(route)/team/[teamId]/_components/TaskSection/TaskSection.tsx +++ b/src/app/(route)/team/[teamId]/_components/TaskSection/TaskSection.tsx @@ -22,7 +22,7 @@ const TaskSection = () => { const todoCount = getUncompletedTaskCount(taskLists); return ( -
          +
          할 일 목록 {todoCount}개 diff --git a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/CreateTaskListModal.tsx b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/CreateTaskListModal.tsx new file mode 100644 index 00000000..8d0d6c1b --- /dev/null +++ b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/CreateTaskListModal.tsx @@ -0,0 +1,51 @@ +import { BaseButton, Input, Modal } from "@/common"; +import { usePostTaskList } from "@/api/hooks"; +import { useParams } from "next/navigation"; +import { ChangeEvent, useState } from "react"; + +interface CreateTaskListModalProps { + isOpen: boolean; + onClose: () => void; +} + +const CreateTaskListModal = ({ isOpen, onClose }: CreateTaskListModalProps) => { + const [taskValue, setTaskValue] = useState(""); + + const { mutate: postTaskList, isPending } = usePostTaskList(); + const { teamId } = useParams(); + + const isDisabledCreateButton = isPending || !taskValue.trim(); + + const handleCreateTaskClick = () => { + postTaskList( + { groupId: Number(teamId), name: taskValue }, + { + onSuccess: () => { + onClose(); + setTaskValue(""); + }, + }, + ); + }; + + return ( + + + +

          할 일 목록

          + ) => setTaskValue(e.target.value)} + placeholder="목록 명을 입력해주세요." + /> +
          + + + {isPending ? "만드는 중..." : "만들기"} + + +
          + ); +}; + +export default CreateTaskListModal; diff --git a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskColumn/TaskColumn.tsx b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskColumn/TaskColumn.tsx index d12294c0..83d1971c 100644 --- a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskColumn/TaskColumn.tsx +++ b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/TaskColumn/TaskColumn.tsx @@ -1,11 +1,10 @@ "use client"; -import { BaseButton, Input, Modal, ProgressButton } from "@/common"; +import { ProgressButton } from "@/common"; import TaskCard from "../TaskCard/TaskCard"; -import { ChangeEvent, useState } from "react"; -import { usePostTaskList } from "@/api/hooks"; -import { useParams } from "next/navigation"; +import { useState } from "react"; import { TaskList } from "@/types"; +import CreateTaskListModal from "../Modal/CreateTaskListModal"; type ColumnTitle = "할 일" | "진행중" | "완료"; @@ -15,25 +14,10 @@ interface TaskColumnProps { } const TaskColumn = ({ title, items }: TaskColumnProps) => { - const { mutate: postTaskList, isPending } = usePostTaskList(); - const { teamId } = useParams(); - const [taskValue, setTaskValue] = useState(""); const [isOpenCreateTaskModal, setIsOpenCreateTaskModal] = useState(false); const isRenderList = title !== "완료"; - const handleCreateTaskClick = () => { - postTaskList( - { groupId: Number(teamId), name: taskValue }, - { - onSuccess: () => { - setIsOpenCreateTaskModal(false); - setTaskValue(""); - }, - }, - ); - }; - return (
          setIsOpenCreateTaskModal(true)} text={title} className="w-full h-[38px]" /> @@ -41,22 +25,7 @@ const TaskColumn = ({ title, items }: TaskColumnProps) => { ))} - setIsOpenCreateTaskModal(false)}> - setIsOpenCreateTaskModal(false)} /> - -

          할 일 목록

          - ) => setTaskValue(e.target.value)} - placeholder="목록 명을 입력해주세요." - /> -
          - - - {isPending ? "만드는 중..." : "만들기"} - - -
          + setIsOpenCreateTaskModal(false)} />
          ); }; From edd02e4f58d032b5c32488ee50a83510ce9d7e7d Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 02:42:05 +0900 Subject: [PATCH 09/19] =?UTF-8?q?refactor:=20=ED=8C=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EA=B8=B0=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/EditTeamForm/EditTeamForm.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx b/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx index 9051107d..c9b446c7 100644 --- a/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx +++ b/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx @@ -1,15 +1,14 @@ "use client"; import { useDevice } from "@/hooks"; -import { ProfileEdit, Input, BaseButton } from "@/common"; +import { ProfileEdit, Input, BaseButton, LinkButton } from "@/common"; import { useGetGroups, usePatchGroup } from "@/api/hooks"; import { ChangeEvent, FormEvent, useState } from "react"; import { useParams } from "next/navigation"; -import Link from "next/link"; const EditTeamForm = () => { const [formData, setFormData] = useState({ - image: null, // TODO(상인): 이미지 편집이 완료되면 구현 string | null + image: null, name: "", }); @@ -18,11 +17,12 @@ const EditTeamForm = () => { const id = Number(teamId); const { data: groups } = useGetGroups({ id }); - const { mutate: patchGroup } = usePatchGroup(); + const { mutate: patchGroup, isPending } = usePatchGroup(); + + const isDisabledEditButton = isPending || !formData.name; const profileSize = isMobile ? "md" : "lg"; - // 이미지 수정에 따라 변경될 수도 const handleFormDataChange = (e: ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); @@ -50,15 +50,12 @@ const EditTeamForm = () => {

          팀 이름은 회사명이나 모임 이름 등으로 설정하면 좋아요.

          - - 수정하기 + + {isPending ? "수정 중..." : "수정하기"} - {/* TODO(상인): 추후 as prop */} - - - 돌아가기 - - + + 돌아가기 +
          ); From 41c7cefc2e61fa4a403f76440dad13bf559304cc Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 03:11:52 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20hooks=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/axios/article/_type.ts | 11 +++++++++ src/api/axios/article/patchArticle.ts | 9 ++++++++ src/api/axios/index.ts | 1 + src/api/hooks/article/usePatchArticle.ts | 23 +++++++++++++++++++ src/api/hooks/index.ts | 1 + .../{_internal => Modal}/ArticleEditModal.tsx | 0 .../_components/_internal/ArticleBody.tsx | 2 +- 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/api/axios/article/patchArticle.ts create mode 100644 src/api/hooks/article/usePatchArticle.ts rename src/app/(route)/dashboard/[id]/_components/{_internal => Modal}/ArticleEditModal.tsx (100%) diff --git a/src/api/axios/article/_type.ts b/src/api/axios/article/_type.ts index 12fb8533..f319f0cb 100644 --- a/src/api/axios/article/_type.ts +++ b/src/api/axios/article/_type.ts @@ -59,3 +59,14 @@ export interface PostArticleRequest { } export type PostArticleResponse = ArticleListItem; + +export interface PatchArticleRequest { + articleId: number; + body: { + image?: string; + content?: string; + title?: string; + }; +} + +export type PatchArticleResponse = ArticleDetail; diff --git a/src/api/axios/article/patchArticle.ts b/src/api/axios/article/patchArticle.ts new file mode 100644 index 00000000..63c7fed8 --- /dev/null +++ b/src/api/axios/article/patchArticle.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { PatchArticleRequest, PatchArticleResponse } from "./_type"; + +const patchArticle = async ({ articleId, body }: PatchArticleRequest) => { + const { data } = await instance.patch(`/articles/${articleId}`, body); + return data; +}; + +export default patchArticle; diff --git a/src/api/axios/index.ts b/src/api/axios/index.ts index 15d5b538..b41b7f2c 100644 --- a/src/api/axios/index.ts +++ b/src/api/axios/index.ts @@ -36,3 +36,4 @@ export { default as deleteArticleComment } from "./articleComment/deleteArticleC export { default as patchArticleComment } from "./articleComment/patchArticleComment"; export { default as getInvitation } from "./group/getInvitation"; export { default as deleteMember } from "./group/deleteMember"; +export { default as patchArticle } from "./article/patchArticle"; diff --git a/src/api/hooks/article/usePatchArticle.ts b/src/api/hooks/article/usePatchArticle.ts new file mode 100644 index 00000000..63983e4b --- /dev/null +++ b/src/api/hooks/article/usePatchArticle.ts @@ -0,0 +1,23 @@ +import { patchArticle } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const usePatchArticle = () => { + const { success, error } = toastKit(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["patchArticle"], + mutationFn: patchArticle, + onSuccess: (data) => { + const articleId = data.id; + success("게시물을 성공적으로 수정하였습니다."); + queryClient.invalidateQueries({ queryKey: ["article", articleId] }); + }, + onError: () => { + error("게시물을 수정하지 못하였습니다."); + }, + }); +}; + +export default usePatchArticle; diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index bcbe1531..25b919cd 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -35,3 +35,4 @@ export { default as useDeleteArticleComment } from "./articleComment/useDeleteAr export { default as usePatchArticleComment } from "./articleComment/usePatchArticleComment"; export { default as useGetInvitation } from "./group/useGetInvitation"; export { default as useDeleteMember } from "./group/useDeleteMember"; +export { default as usePatchArticle } from "./article/usePatchArticle"; diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleEditModal.tsx b/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx similarity index 100% rename from src/app/(route)/dashboard/[id]/_components/_internal/ArticleEditModal.tsx rename to src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx index bd5e22fd..9443348f 100644 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx +++ b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx @@ -8,7 +8,7 @@ import ArticleTitle from "../../../_components/Article/_internal/ArticleTitle"; import ArticleWriter from "../../../_components/Article/_internal/ArticleWriter"; import ArticleContent from "../../../_components/Article/_internal/ArticleContent"; import ArticleLikeButton from "./ArticleLikeButton"; -import ArticleEditModal from "./ArticleEditModal"; +import ArticleEditModal from "../Modal/ArticleEditModal"; import { useState } from "react"; const ArticleBody = () => { From c4ee3a23ad1c7f1a75a515e21830f12e91141cde Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:06:37 +0900 Subject: [PATCH 11/19] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/axios/article/_type.ts | 2 +- .../_components/Modal/ArticleEditModal.tsx | 144 +++++++++++++++++- .../_components/_internal/ArticleBody.tsx | 9 +- src/hooks/useImageUpload.ts | 10 ++ 4 files changed, 159 insertions(+), 6 deletions(-) diff --git a/src/api/axios/article/_type.ts b/src/api/axios/article/_type.ts index f319f0cb..f37e2141 100644 --- a/src/api/axios/article/_type.ts +++ b/src/api/axios/article/_type.ts @@ -63,7 +63,7 @@ export type PostArticleResponse = ArticleListItem; export interface PatchArticleRequest { articleId: number; body: { - image?: string; + image?: string | null; content?: string; title?: string; }; diff --git a/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx b/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx index 29ef4832..bb14eb87 100644 --- a/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx +++ b/src/app/(route)/dashboard/[id]/_components/Modal/ArticleEditModal.tsx @@ -1,16 +1,152 @@ -import { Modal } from "@/common"; +"use client"; + +import { usePatchArticle } from "@/api/hooks"; +import { BaseButton, Input, InputBox, Modal, Icon, FloatingButton } from "@/common"; +import { ArticleDetail } from "@/types/ArticleType"; +import { ChangeEvent, useState } from "react"; +import Image from "next/image"; +import useImageUpload from "@/hooks/useImageUpload"; interface ArticleEditModalProps { isOpen: boolean; onClose: () => void; + article: ArticleDetail; +} + +interface FormStateType { + title: string; + content: string; + image: string | null; } -const ArticleEditModal = ({ isOpen, onClose }: ArticleEditModalProps) => { +const ArticleEditModal = ({ isOpen, onClose, article }: ArticleEditModalProps) => { + const { mutate: patchArticle, isPending } = usePatchArticle(); + + const [formState, setFormState] = useState({ + title: article.title, + content: article.content, + image: article.image, + }); + + const { preview, file, handleImageChange, uploadImage, isUploading, clear } = useImageUpload( + article.image ?? undefined, + ); + + const isDisabledEditButton = isPending || isUploading || !formState.title.trim() || !formState.content.trim(); + + const handleTextChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormState((prev) => ({ ...prev, [name]: value })); + }; + + const handleRemoveImage = () => { + clear(); + setFormState((prev) => ({ + ...prev, + image: null, + })); + }; + + const handleEditClick = async () => { + const { title, content } = formState; + + let imageUrl: string | null = null; + + if (file) { + imageUrl = await uploadImage(); + } + const newForm = { + title, + content, + image: imageUrl !== null ? imageUrl : formState.image === null ? null : formState.image, + }; + patchArticle( + { + articleId: article.id, + body: newForm, + }, + { + onSuccess: () => { + onClose(); + }, + }, + ); + }; + return ( - -

          게시글 수정하기

          + +

          게시글 수정하기

          + +
          + + + + + +
          + + { + const selectedFile = e.target.files?.[0]; + if (selectedFile) handleImageChange(selectedFile); + }} + /> + +
          + + + {preview && ( + + )} +
          +
          +
          + + + + 닫기 + + + + {isPending || isUploading ? "수정 중..." : "수정하기"} + +
          ); }; diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx index 9443348f..154222ff 100644 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx +++ b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx @@ -51,7 +51,14 @@ const ArticleBody = () => { - setIsOpenEditModal(false)} /> + {isOpenEditModal && ( + setIsOpenEditModal(false)} + article={article} + /> + )}
      ); }; diff --git a/src/hooks/useImageUpload.ts b/src/hooks/useImageUpload.ts index 77d852e7..a358a118 100644 --- a/src/hooks/useImageUpload.ts +++ b/src/hooks/useImageUpload.ts @@ -10,6 +10,7 @@ interface UsePostImageUploadReturn { uploadImage: () => Promise; isUploading: boolean; reset: () => void; + clear: () => void; } const useImageUpload = (initialImage?: string): UsePostImageUploadReturn => { @@ -34,6 +35,14 @@ const useImageUpload = (initialImage?: string): UsePostImageUploadReturn => { setPreview(initialImage || ""); }; + const clear = () => { + if (preview && !preview.startsWith("http")) { + URL.revokeObjectURL(preview); + } + setFile(null); + setPreview(""); + }; + useEffect(() => { return () => { if (preview && !preview.startsWith("http")) { @@ -69,6 +78,7 @@ const useImageUpload = (initialImage?: string): UsePostImageUploadReturn => { uploadImage, isUploading: uploadMutation.isPending, reset, + clear, }; }; From 26d4b7c7ba2fcf3f35b62d8baed9377dcb4ec1b1 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:16:57 +0900 Subject: [PATCH 12/19] =?UTF-8?q?refactor:=20=EA=B8=80=EC=93=B0=EA=B8=B0?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20useImageUpload=20=ED=9B=85=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../write/_components/ArticleForm.tsx | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/src/app/(route)/dashboard/write/_components/ArticleForm.tsx b/src/app/(route)/dashboard/write/_components/ArticleForm.tsx index c2d67fd0..8602f7b2 100644 --- a/src/app/(route)/dashboard/write/_components/ArticleForm.tsx +++ b/src/app/(route)/dashboard/write/_components/ArticleForm.tsx @@ -1,33 +1,32 @@ "use client"; import Image from "next/image"; -import { Input, InputBox, BaseButton, Icon } from "@/common"; +import { Input, InputBox, BaseButton, Icon, FloatingButton } from "@/common"; import { ChangeEvent, FormEvent, useState } from "react"; -import { postImageUpload } from "@/api/axios"; import { usePostArticle } from "@/api/hooks"; import { INPUT_AREA_STYLE, LABEL_STYLE } from "../_constants/STYLE"; -import { MAX_IMAGE_SIZE } from "../_constants/MAX_IMAGE_SIZE"; import { toastKit } from "@/utils"; +import useImageUpload from "@/hooks/useImageUpload"; interface FormStateType { title: string; content: string; - image: File | null; } const ArticleForm = () => { const { mutate: postArticle, isPending } = usePostArticle(); - const [preview, setPreview] = useState(null); + const [formState, setFormState] = useState({ title: "", content: "", - image: null, }); - const isSubmitDisabled = isPending || !formState.title.trim() || !formState.content.trim(); + const { preview, file, handleImageChange, uploadImage, isUploading, clear } = useImageUpload(); const { error } = toastKit(); + const isSubmitDisabled = isPending || isUploading || !formState.title.trim() || !formState.content.trim(); + const handleTextChange = (e: ChangeEvent) => { const { name, value } = e.target; setFormState((prev) => ({ @@ -36,44 +35,32 @@ const ArticleForm = () => { })); }; - const handleImageChange = (e: ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) { - return; - } - if (file.size > MAX_IMAGE_SIZE) { - error("이미지 파일, 최대 용량은 10MB입니다."); - e.target.value = ""; - return; - } - - if (preview) { - URL.revokeObjectURL(preview); - } - - const previewUrl = URL.createObjectURL(file); - setPreview(previewUrl); - setFormState((prev) => ({ - ...prev, - image: file, - })); + const handleRemoveImage = () => { + clear(); }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); - const { title, content, image } = formState; + const { title, content } = formState; try { - const imageURL = image && (await postImageUpload(image)); + let imageUrl: string | null = null; + + if (file) { + imageUrl = await uploadImage(); + } + const newFormData = { - title: title, - content: content, - ...(imageURL && { image: imageURL }), + title, + content, + ...(imageUrl && { image: imageUrl }), }; + postArticle(newFormData); - } catch (error) { - console.error(error); + } catch (err) { + console.error(err); + error("게시글 등록 중 오류가 발생했습니다."); } }; @@ -110,23 +97,45 @@ const ArticleForm = () => {
      - - -
      + - {isPending ? "등록 중..." : "등록하기"} + {isPending || isUploading ? "등록 중..." : "등록하기"} ); From 71fe8920cbfae634bbf60f073be10cbceb42ec08 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:22:27 +0900 Subject: [PATCH 13/19] =?UTF-8?q?refactor:=20=EA=B8=80=EC=93=B0=EA=B8=B0?= =?UTF-8?q?=20=ED=9B=84=20=EB=AA=A9=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EB=8F=8C?= =?UTF-8?q?=EC=95=84=EC=99=94=EC=9D=84=20=EB=95=8C=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=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/api/hooks/article/usePostArticle.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/hooks/article/usePostArticle.ts b/src/api/hooks/article/usePostArticle.ts index 9de6c36a..0452c011 100644 --- a/src/api/hooks/article/usePostArticle.ts +++ b/src/api/hooks/article/usePostArticle.ts @@ -1,15 +1,17 @@ import { postArticle } from "@/api/axios"; import { toastKit } from "@/utils"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; const usePostArticle = () => { const router = useRouter(); + const queryClient = useQueryClient(); const { success, error } = toastKit(); return useMutation({ mutationFn: postArticle, onSuccess: (data) => { success("게시물 등록을 성공했습니다."); + queryClient.invalidateQueries({ queryKey: ["articles"] }); router.replace(`/dashboard/${data.id}`); }, onError: () => { From 4980d665adae25e32b0ea3b8d2aa37b83f9c1156 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:55:03 +0900 Subject: [PATCH 14/19] =?UTF-8?q?refactor:=20=ED=8C=80=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hooks/group/usePatchGroup.ts | 3 - .../_components/EditTeamForm/EditTeamForm.tsx | 81 +++++++++--------- .../team/[teamId]/edit/_hooks/index.ts | 1 + .../team/[teamId]/edit/_hooks/useTeamEdit.ts | 82 +++++++++++++++++++ 4 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 src/app/(route)/team/[teamId]/edit/_hooks/index.ts create mode 100644 src/app/(route)/team/[teamId]/edit/_hooks/useTeamEdit.ts diff --git a/src/api/hooks/group/usePatchGroup.ts b/src/api/hooks/group/usePatchGroup.ts index 32be4f9b..1695459f 100644 --- a/src/api/hooks/group/usePatchGroup.ts +++ b/src/api/hooks/group/usePatchGroup.ts @@ -1,18 +1,15 @@ import { patchGroup } from "@/api/axios"; import { toastKit } from "@/utils"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useRouter } from "next/navigation"; const usePatchGroup = () => { const { success, error } = toastKit(); - const router = useRouter(); const queryClient = useQueryClient(); return useMutation({ mutationFn: patchGroup, onSuccess: () => { success("팀 이름을 성공적으로 변경하였습니다."); queryClient.invalidateQueries({ queryKey: ["groups"] }); - router.back(); }, onError: () => { error("팀 이름을 변경하지 못했습니다."); diff --git a/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx b/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx index c9b446c7..106782ce 100644 --- a/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx +++ b/src/app/(route)/team/[teamId]/edit/_components/EditTeamForm/EditTeamForm.tsx @@ -1,64 +1,71 @@ "use client"; +import { FormEvent } from "react"; import { useDevice } from "@/hooks"; -import { ProfileEdit, Input, BaseButton, LinkButton } from "@/common"; -import { useGetGroups, usePatchGroup } from "@/api/hooks"; -import { ChangeEvent, FormEvent, useState } from "react"; +import { ProfileEdit, Input, BaseButton, FloatingButton } from "@/common"; +import { useTeamEdit } from "../../_hooks"; import { useParams } from "next/navigation"; -const EditTeamForm = () => { - const [formData, setFormData] = useState({ - image: null, - name: "", - }); - +const TeamEditForm = () => { const { isMobile } = useDevice(); + const profileSize = isMobile ? "md" : "lg"; const { teamId } = useParams(); const id = Number(teamId); - const { data: groups } = useGetGroups({ id }); - - const { mutate: patchGroup, isPending } = usePatchGroup(); - const isDisabledEditButton = isPending || !formData.name; + const { + name, + errorMessage, + preview, + isValid, + isSubmitting, + handleNameChange, + handleImageChange, + handleSubmit, + handleRemoveImage, + } = useTeamEdit(id); - const profileSize = isMobile ? "md" : "lg"; - - const handleFormDataChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); - }; - - const handleEditSubmit = (e: FormEvent) => { + const onSubmit = (e: FormEvent) => { e.preventDefault(); - patchGroup({ param: { id }, body: formData }); + handleSubmit(); }; return ( -
      +
      - {}} size={profileSize} /> +
      + + {preview && ( + + )} +
      handleNameChange(e.target.value)} + error={errorMessage} + minLength={2} + maxLength={30} />
      -
      -

      + +

      + + {isSubmitting ? "수정 중..." : "수정하기"} + + +

      팀 이름은 회사명이나 모임 이름 등으로 설정하면 좋아요.

      - - {isPending ? "수정 중..." : "수정하기"} - - - 돌아가기 -
      ); }; -export default EditTeamForm; +export default TeamEditForm; diff --git a/src/app/(route)/team/[teamId]/edit/_hooks/index.ts b/src/app/(route)/team/[teamId]/edit/_hooks/index.ts new file mode 100644 index 00000000..faeb8130 --- /dev/null +++ b/src/app/(route)/team/[teamId]/edit/_hooks/index.ts @@ -0,0 +1 @@ +export { default as useTeamEdit } from "./useTeamEdit"; diff --git a/src/app/(route)/team/[teamId]/edit/_hooks/useTeamEdit.ts b/src/app/(route)/team/[teamId]/edit/_hooks/useTeamEdit.ts new file mode 100644 index 00000000..b6b5fe0f --- /dev/null +++ b/src/app/(route)/team/[teamId]/edit/_hooks/useTeamEdit.ts @@ -0,0 +1,82 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useGetGroups, usePatchGroup } from "@/api/hooks"; +import useImageUpload from "@/hooks/useImageUpload"; + +const useTeamEdit = (teamId: number) => { + const router = useRouter(); + const { data: group } = useGetGroups({ id: teamId }); + const { mutateAsync: patchGroup } = usePatchGroup(); + + const [name, setName] = useState(group?.name ?? ""); + const [errorMessage, setErrorMessage] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const originImage = group?.image ?? null; + + const { preview, file, handleImageChange, uploadImage, clear } = useImageUpload(originImage ?? undefined); + + const [isImageRemoved, setIsImageRemoved] = useState(false); + + const handleNameChange = (value: string) => { + setName(value); + + if (value.trim().length < 2) { + setErrorMessage("팀 이름은 2자 이상 입력해주세요."); + return; + } + + setErrorMessage(""); + }; + + const isValid = name.trim().length >= 2 && !errorMessage; + + const handleRemoveImage = () => { + clear(); + setIsImageRemoved(true); + }; + + const handleSubmit = async () => { + if (!isValid) return; + + try { + setIsSubmitting(true); + + let imageUrl: string | null = null; + if (file) { + imageUrl = await uploadImage(); + } + + const body = { + name, + ...(imageUrl && { image: imageUrl }), + ...(isImageRemoved && !imageUrl && { image: null }), + }; + + await patchGroup({ + param: { id: teamId }, + body, + }); + + router.push(`/team/${teamId}`); + } finally { + setIsSubmitting(false); + } + }; + + return { + name, + errorMessage, + preview, + isValid, + isSubmitting, + handleNameChange, + handleImageChange, + handleRemoveImage, + handleSubmit, + }; +}; + +export default useTeamEdit; From 7eeeb8400ace598b006cc92a2a0c41230d530e25 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:58:23 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=ED=95=A0=20=EC=9D=BC=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TaskSection/_internal/Modal/EditTaskListModal.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/EditTaskListModal.tsx b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/EditTaskListModal.tsx index 055d9170..bf233d23 100644 --- a/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/EditTaskListModal.tsx +++ b/src/app/(route)/team/[teamId]/_components/TaskSection/_internal/Modal/EditTaskListModal.tsx @@ -15,11 +15,16 @@ const EditTaskListModal = ({ isOpen, onClose, groupId, taskList }: EditTaskListM const [title, setTitle] = useState(""); + const isDisabledEditButton = isPending || !title.trim(); + const handleEditClick = () => { patchTask( { groupId, id: taskList.id, name: title }, { - onSuccess: () => onClose(), + onSuccess: () => { + onClose(); + setTitle(""); + }, }, ); }; @@ -35,7 +40,7 @@ const EditTaskListModal = ({ isOpen, onClose, groupId, taskList }: EditTaskListM /> - + {isPending ? "수정 중..." : "수정하기"} From ff3b8b801eb2f9236d66e7ab9c9c93cebe586b36 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:05:14 +0900 Subject: [PATCH 16/19] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[id]/_components/_internal/ArticleBody.tsx | 2 +- .../_components/Article/_internal/ArticleContent.tsx | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx index 154222ff..4bbd0e51 100644 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx +++ b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx @@ -46,7 +46,7 @@ const ArticleBody = () => {
      - +
      diff --git a/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx b/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx index 7e119615..ff506066 100644 --- a/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx +++ b/src/app/(route)/dashboard/_components/Article/_internal/ArticleContent.tsx @@ -1,5 +1,12 @@ -const ArticleContent = ({ content }: { content: string }) => { - return

      {content}

      ; +import Image from "next/image"; + +const ArticleContent = ({ content, image }: { content: string; image: string | null }) => { + return ( +
      +

      {content}

      + {image && 게시글 이미지} +
      + ); }; export default ArticleContent; From 4faf5c00d31beb6e71c32ab6d8e6e509810161e9 Mon Sep 17 00:00:00 2001 From: SanginJeong <144919938+SanginJeong@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:31:29 +0900 Subject: [PATCH 17/19] =?UTF-8?q?fix:=20=ED=97=88=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20URL=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/_internal/ArticleBody.tsx | 2 +- .../_components/Article/BestArticleCard.tsx | 10 ++--- .../Article/DefaultArticleCard.tsx | 5 +-- .../Article/_internal/ArticleContent.tsx | 38 +++++++++++++++++-- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx index 4bbd0e51..5d18ee31 100644 --- a/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx +++ b/src/app/(route)/dashboard/[id]/_components/_internal/ArticleBody.tsx @@ -46,7 +46,7 @@ const ArticleBody = () => {
- +
diff --git a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx index 85442ea3..8208028b 100644 --- a/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx +++ b/src/app/(route)/dashboard/_components/Article/BestArticleCard.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import Image from "next/image"; import { useGetArticle } from "@/api/hooks"; import { Icon } from "@/common"; import ArticleBestBadge from "./_internal/ArticleBestBadge"; @@ -20,12 +19,9 @@ const BestArticleCard = ({ articleId }: { articleId: number }) => {
-
-
- - -
- {/* {article.image && 게시글 이미지} */} +
+ +