From 0361cef34f99154abc4a594bfc870cae860ab8dd Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 14:05:12 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=BC=ED=8C=A8=EB=84=90=20=EB=82=B4=20?= =?UTF-8?q?KPI=EC=A7=80=ED=91=9C=20=ED=95=9C=EC=A4=84=EC=97=90=20max=202?= =?UTF-8?q?=EA=B0=9C,=20=ED=99=80=EC=88=98=EA=B0=9C=EC=88=98=EB=8A=94=20?= =?UTF-8?q?=EB=A7=88=EC=A7=80=EB=A7=89=EB=A7=8C=20col-span-2=EB=A1=9C=20?= =?UTF-8?q?=ED=95=9C=EC=A4=84=20=EB=8B=A4=20=EC=B0=A8=EC=A7=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/timeline/TimelinePerformancePanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index bc9147d3..1fa9776c 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -350,13 +350,14 @@ export default function TimelinePerformancePanel({ {/* KPI — 선택한 지표 수만큼 가로 균등 분할 */} -
+
{data.metrics.map((metric) => (
From 921ef6847b8afbf052d4c3ced66813d9025fcefe Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 14:51:31 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=94=20=EB=93=9C=EB=9E=8D=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EC=9D=B4=20=ED=99=94=EB=A9=B4=20=EB=B0=96=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=82=98=EA=B0=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=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 --- .../common/dropdownmenu/DropdownMenu.tsx | 75 +++++++++++++++---- src/components/timeline/TimelineBar.tsx | 6 +- src/pages/dashboard/timeline/Timeline.tsx | 1 + 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index 8e196e53..3dfce7b1 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -24,10 +24,13 @@ const easeIn = [0.4, 0, 1, 1] as const; const MENU_ITEM_HEIGHT = 52; const MENU_VERTICAL_PADDING = 32; +const DEFAULT_MENU_WIDTH = 224; function getClippingBounds(element: HTMLElement) { let bottom = window.innerHeight; let top = 0; + let left = 0; + let right = window.innerWidth; let parent = element.parentElement; while (parent) { @@ -45,12 +48,14 @@ function getClippingBounds(element: HTMLElement) { const rect = parent.getBoundingClientRect(); bottom = Math.min(bottom, rect.bottom); top = Math.max(top, rect.top); + left = Math.max(left, rect.left); + right = Math.min(right, rect.right); } parent = parent.parentElement; } - return { top, bottom }; + return { top, bottom, left, right }; } function resolveAutoPlacement( @@ -65,11 +70,37 @@ function resolveAutoPlacement( const spaceBelow = containerBottom - triggerRect.bottom; const spaceAbove = triggerRect.top - containerTop; - return spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow + const visibleHeight = containerBottom - containerTop; + + const inLowerArea = + visibleHeight > 0 && + triggerRect.bottom > containerTop + visibleHeight * 0.65; + + if (inLowerArea && spaceAbove >= estimatedMenuHeight) { + return "top"; + } + + return spaceBelow < estimatedMenuHeight && spaceAbove >= estimatedMenuHeight ? "top" : "bottom"; } +function resolveHorizontalAlign( + element: HTMLElement, + menuWidth = DEFAULT_MENU_WIDTH, +): "left" | "right" { + const triggerRect = element.getBoundingClientRect(); + const { left: containerLeft, right: containerRight } = + getClippingBounds(element); + const spaceLeft = triggerRect.right - containerLeft; + const spaceRight = containerRight - triggerRect.left; + + if (spaceLeft < menuWidth && spaceRight > spaceLeft) { + return "left"; + } + return "right"; +} + export function DropdownMenu({ trigger, items, @@ -98,6 +129,9 @@ export function DropdownMenu({ const [resolvedPlacement, setResolvedPlacement] = useState<"bottom" | "top">( "bottom", ); + const [horizontalAlign, setHorizontalAlign] = useState<"left" | "right">( + "right", + ); const ref = useRef(null); const menuId = useId(); @@ -118,30 +152,34 @@ export function DropdownMenu({ useLayoutEffect(() => { if (!open || inFlow) { setResolvedPlacement("bottom"); + setHorizontalAlign("right"); return; } + const el = ref.current; + if (!el) return; + if (placement === "bottom") { setResolvedPlacement("bottom"); - return; - } - - if (placement === "top") { + } else if (placement === "top") { setResolvedPlacement("top"); - return; + } else { + setResolvedPlacement(resolveAutoPlacement(el, items.length)); } - const el = ref.current; - if (!el) return; - - setResolvedPlacement(resolveAutoPlacement(el, items.length)); - }, [open, placement, items.length, inFlow]); + if (!fullWidth) { + setHorizontalAlign(resolveHorizontalAlign(el)); + } else { + setHorizontalAlign("right"); + } + }, [open, placement, items.length, inFlow, fullWidth]); useEffect(() => { onOpenChange?.(open); }, [open, onOpenChange]); const isTopPlacement = !inFlow && resolvedPlacement === "top"; + const isLeftAlign = !inFlow && !fullWidth && horizontalAlign === "left"; return (
void; onEdit?: (bar: ITimelineCampaignBar) => void; onDelete?: (bar: ITimelineCampaignBar) => void; + menuPlacement?: "bottom" | "top" | "auto"; } export default function TimelineBar({ @@ -40,6 +41,7 @@ export default function TimelineBar({ onBarClick, onEdit, onDelete, + menuPlacement = "auto", }: ITimelineBarProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const status = resolveTimelinePerformanceStatusStyle(bar.performanceStatus); @@ -127,7 +129,7 @@ export default function TimelineBar({ {showActions ? (
handleEditTimeline(bar.id)} From eef4f2ebb4c4b3def0727ef153eeed52195309be Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 15:13:11 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=A7=84?= =?UTF-8?q?=ED=96=89,=20=EC=BC=9C=EC=A7=90=20=EB=B1=83=EC=A7=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/NotificationSection.tsx | 108 ++++++++---------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/src/components/setting/NotificationSection.tsx b/src/components/setting/NotificationSection.tsx index 8f044dee..fe67d352 100644 --- a/src/components/setting/NotificationSection.tsx +++ b/src/components/setting/NotificationSection.tsx @@ -1,4 +1,3 @@ -import Badge from "@/components/common/badge/Badge"; import Button from "@/components/common/button/Button"; import Card from "@/components/common/card/Card"; import Input from "@/components/common/input/Input"; @@ -87,23 +86,20 @@ export default function NotificationSection({ const isDiscordPending = pendingOrgAction === "discord"; const isAnyOrgPending = pendingOrgAction != null; return ( - +

알림 설정

-
-
-

마스터 알림

-

+

+
+

마스터 알림

+

끄면 모든 알림이 비활성화 됩니다

-
- {masterEnabled && !workspaceNotifiDisabled && ( - 켜짐 - )} +
-
-
+
+
-

브라우저 푸시 알림

+

+ 브라우저 푸시 알림 +

-
- {browserPush && !channelDisabled && ( - 켜짐 - )} +
-
-
+
+
-
-

이메일 알림 설정

-

{email}

+
+

+ 이메일 알림 설정 +

+

{email}

-
- {emailNotif && !channelDisabled && ( - 켜짐 - )} +
{isAdmin && ( -
+
-

슬랙 연동하기

-

+

+ 슬랙 연동하기 +

+

{slackConnected ? "연동됨 • 알림 on/off는 저장 시 반영" : "Webhook URL로 연동"} @@ -174,10 +170,7 @@ export default function NotificationSection({

{slackConnected ? ( -
- {slackEnabled && !channelDisabled && ( - 켜짐 - )} +
) : ( - <> +
@@ -213,7 +206,7 @@ export default function NotificationSection({ variant="outline" size="small" type="button" - className="shrink-0" + className="shrink-0 tablet:w-full" disabled={ channelDisabled || isAnyOrgPending || @@ -224,20 +217,20 @@ export default function NotificationSection({ > 연동 - +
)}
)} {isAdmin && ( -
+
-

+

디스코드 연동하기

-

+

{discordConnected ? "연동됨 • 알림 on/off는 저장 시 반영" : "Webhook URL로 연동"} @@ -245,10 +238,7 @@ export default function NotificationSection({

{discordConnected ? ( -
- {discordEnabled && !channelDisabled && ( - 켜짐 - )} +
) : ( - <> +
@@ -284,7 +274,7 @@ export default function NotificationSection({ variant="outline" size="small" type="button" - className="shrink-0" + className="shrink-0 tablet:w-full" disabled={ channelDisabled || isAnyOrgPending || @@ -295,7 +285,7 @@ export default function NotificationSection({ > 연동 - +
)}
)} @@ -324,16 +314,15 @@ export default function NotificationSection({
-

클릭수 알림

-

+

+ 클릭수 알림 +

+

이상 징후가 있을 때 알림

-
- {clickAlarm && !channelDisabled && ( - 켜짐 - )} +
-

주간 리포트

-

+

+ 주간 리포트 +

+

매주 월요일 오전 8시 리포트 이메일 발송

-
- {weeklyReport && !channelDisabled && ( - 켜짐 - )} +
Date: Mon, 10 Aug 2026 15:16:54 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EB=A9=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/workspace/MemberItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/workspace/MemberItem.tsx b/src/components/workspace/MemberItem.tsx index 87f9a171..a2d0a3df 100644 --- a/src/components/workspace/MemberItem.tsx +++ b/src/components/workspace/MemberItem.tsx @@ -42,7 +42,7 @@ export default function MemberItem({ !isReceiveUpdating && !!onReceiveToggle; return ( -
  • +
  • {member.profileImageUrl ? ( From f65e3dd224aabdb1881ff0d5a02278c6c1205022 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 15:20:56 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EA=B6=8C=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/workspace/PermissionTable.tsx | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/workspace/PermissionTable.tsx b/src/components/workspace/PermissionTable.tsx index eec0e911..6e2d96a3 100644 --- a/src/components/workspace/PermissionTable.tsx +++ b/src/components/workspace/PermissionTable.tsx @@ -110,25 +110,27 @@ export default function PermissionTable() { }; return ( - +

    권한 설정

    -

    +

    역할별 권한을 확인하고 설정할 수 있습니다

    -
    - +
    +
    - - + @@ -139,18 +141,20 @@ export default function PermissionTable() { > - -
    기능 및 작업 + 관리자 멤버 + 멤버 +
    -

    {row.label}

    -

    +

    + {row.label} +

    +

    {row.description}

    +
    +
    Date: Mon, 10 Aug 2026 15:23:03 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20Card=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/setting/PasswordSection.tsx | 2 +- src/components/setting/ProfileSection.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/setting/PasswordSection.tsx b/src/components/setting/PasswordSection.tsx index 7c37c300..8ac6e6de 100644 --- a/src/components/setting/PasswordSection.tsx +++ b/src/components/setting/PasswordSection.tsx @@ -36,7 +36,7 @@ export default function PasswordSection({ const [showConfirm, setShowConfirm] = useState(false); return ( - +
    diff --git a/src/components/setting/ProfileSection.tsx b/src/components/setting/ProfileSection.tsx index d7c300c1..ad7c8d04 100644 --- a/src/components/setting/ProfileSection.tsx +++ b/src/components/setting/ProfileSection.tsx @@ -32,7 +32,7 @@ export default function ProfileSection({ resetImage, }: TProfileSectionProps) { return ( - +
    From 7b8835ebad7a36d6f8dbcf8876b5af5315f6f57d Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 15:27:40 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=20=EB=B9=84?= =?UTF-8?q?=EB=8C=80=EC=B9=AD=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/modal/Modal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/common/modal/Modal.tsx b/src/components/common/modal/Modal.tsx index 27e05117..490c298d 100644 --- a/src/components/common/modal/Modal.tsx +++ b/src/components/common/modal/Modal.tsx @@ -195,8 +195,6 @@ function Modal({ "relative mx-4 w-full max-h-[90vh] overflow-auto rounded-2xl bg-surface-100 shadow-Soft", sizeClasses[size], paddingClasses[padding], - // absolute 닫기 버튼(top-4 right-4)과 본문 겹침 방지 - !hideCloseButton && "pr-12", className, )} initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.95 }} From 9fa772a6574af284017e578fc1ffcb57218a17cd Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Mon, 10 Aug 2026 15:46:11 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EC=A1=B0=EC=A7=81=20=EC=86=8C?= =?UTF-8?q?=EC=9C=A0=EA=B6=8C=20=EC=96=91=EB=8F=84=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=98=EC=9D=91=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/select/SearchSelect.tsx | 7 +++++-- src/components/workspace/MemberSearchSelect.tsx | 3 +++ src/components/workspace/TransferOwnerModal.tsx | 8 ++++---- src/pages/workspace/Workspace.tsx | 7 ++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/common/select/SearchSelect.tsx b/src/components/common/select/SearchSelect.tsx index 943b7235..6e595f26 100644 --- a/src/components/common/select/SearchSelect.tsx +++ b/src/components/common/select/SearchSelect.tsx @@ -112,9 +112,12 @@ export default function SearchSelect({ aria-controls={listboxId} aria-haspopup="listbox" aria-autocomplete="list" - className="h-13 w-full rounded-2xl border border-info-blue px-4 pl-5 font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-info-blue" + containerClassName="h-13 w-full rounded-2xl border border-info-blue font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-info-blue" + inputClassName="truncate" + rightElement={ + + } /> -
    {isOpen && ( diff --git a/src/components/workspace/MemberSearchSelect.tsx b/src/components/workspace/MemberSearchSelect.tsx index 665ea38c..c7aed90f 100644 --- a/src/components/workspace/MemberSearchSelect.tsx +++ b/src/components/workspace/MemberSearchSelect.tsx @@ -11,6 +11,7 @@ type TMemberSearchSelectProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; placeholder?: string; + className?: string; }; export default function MemberSearchSelect({ @@ -20,6 +21,7 @@ export default function MemberSearchSelect({ isOpen, onOpenChange, placeholder = "소유권을 양도할 멤버를 검색하세요", + className, }: TMemberSearchSelectProps) { return ( )} emptyMessage="검색 결과가 없습니다" + className={className} /> ); } diff --git a/src/components/workspace/TransferOwnerModal.tsx b/src/components/workspace/TransferOwnerModal.tsx index e2a11ddf..1c58012c 100644 --- a/src/components/workspace/TransferOwnerModal.tsx +++ b/src/components/workspace/TransferOwnerModal.tsx @@ -57,14 +57,14 @@ export default function TransferOwnerModal({
    -

    +

    조직 소유권을 양도할까요?

    -

    +

    현재 소유자: {currentOwnerName}

    -

    - 양도 대상은 같은 조직의 관리자(ADMIN)만 선택할 수 있습니다. +

    + 양도 대상은 같은 조직의 관리자만 선택할 수 있습니다.
    양도 후에는 되돌릴 수 없으며, 새 소유자만 다시 양도 가능합니다.

    diff --git a/src/pages/workspace/Workspace.tsx b/src/pages/workspace/Workspace.tsx index 3ef55bce..5b129d23 100644 --- a/src/pages/workspace/Workspace.tsx +++ b/src/pages/workspace/Workspace.tsx @@ -228,9 +228,9 @@ export default function WorkspacePage() {

    워크스페이스 생성

    -

    +

    워크스페이스를 생성한 사용자는 자동으로 관리자 권한을 갖습니다. -
    +
    로고 이미지와 기본 정보를 입력해 주세요.

    @@ -294,13 +294,14 @@ export default function WorkspacePage() { value={newDesc} onChange={(e) => setNewDesc(e.target.value)} disabled={isCreating} + className="break-keep" /> {createErrorMsg && (

    {createErrorMsg}

    )}
    -
    +
    + +
    +
    + + ); +} diff --git a/src/pages/workspace/WorkspaceSetting.tsx b/src/pages/workspace/WorkspaceSetting.tsx index be117742..5e9d96f8 100644 --- a/src/pages/workspace/WorkspaceSetting.tsx +++ b/src/pages/workspace/WorkspaceSetting.tsx @@ -9,9 +9,8 @@ import Card from "@/components/common/card/Card"; import AreaErrorFallback from "@/components/common/error/AreaErrorFallback"; import { ErrorBoundary } from "@/components/common/error/ErrorBoundary"; import Input from "@/components/common/input/Input"; -import Modal from "@/components/common/modal/Modal"; -import ModalContent from "@/components/common/modal/ModalContent"; import TextareaField from "@/components/common/textarea/TextareaField"; +import DeleteWorkspaceModal from "@/components/workspace/DeleteWorkspaceModal"; import TransferOwnerModal from "@/components/workspace/TransferOwnerModal"; import WorkspaceSettingLoading from "@/components/workspace/WorkspaceSettingLoading"; @@ -25,7 +24,6 @@ import { updateWorkspace, } from "@/api/workspace/org"; import BuildingIcon from "@/assets/icon/common/building.svg?react"; -import WarnIcon from "@/assets/icon/common/warn-circle.svg?react"; import { getImageUrl } from "@/lib/getImageUrl"; import { QUERY_KEYS } from "@/lib/queryKeys"; @@ -426,51 +424,18 @@ export default function WorkspaceSetting() { )} - { if (!deleting) { setDeleteOpen(false); setDeleteNameSnapshot(""); - setDeleteConfirmInput(""); } }} - title="워크스페이스를 삭제할게요" - size="lg" - disableOverlayClick={deleting} - > - + workspaceName={deleteNameSnapshot} + onConfirm={onDelete} + isLoading={deleting} + />