Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions dpbr_front/app/src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import { toast } from "$lib/stores/toast";

interface Props {
variant?: "main" | "detail" | "close" | "save";
title?: string;
Expand All @@ -18,6 +20,39 @@
onCloseClick,
onTalkClick,
}: Props = $props();

let logoClickCount = $state(0);
let lastLogoClickTime = 0;
let isEasterEggActive = false;

function handleLogoClick() {
if (isEasterEggActive) return;

const now = Date.now();
if (now - lastLogoClickTime < 500) {
logoClickCount += 1;
} else {
logoClickCount = 1;
}
lastLogoClickTime = now;

if (logoClickCount === 3) {
isEasterEggActive = true;
const credits = [
"Fullstack DevOps & PM\n강민",
"\nDesigner & PM\n강민아",
"\nFrontend Engineer\n황현성",
"\nBackend Engineer\n배승민 서민성 이서윤",
"\nServer Engineer\n김형규",
].join("\n");
toast.show(credits, 5000, "center");
logoClickCount = 0;

setTimeout(() => {
isEasterEggActive = false;
}, 5000);
}
}
</script>

{#if variant === "main"}
Expand All @@ -38,12 +73,19 @@
draggable="false"
/>
</button>
<img
src="/images/logos/logo-text-white.svg"
alt="단풍바람"
class="h-[18px] object-contain"
draggable="false"
/>
<button
type="button"
onclick={handleLogoClick}
class="flex items-center justify-center h-full active:scale-95 transition-transform"
aria-label="단풍바람 로고"
>
<img
src="/images/logos/logo-text-white.svg"
alt="단풍바람"
class="h-[18px] object-contain"
draggable="false"
/>
</button>
<a href="/talk" class="p-2 text-white" aria-label="톡 페이지">
<img
src="/images/icons/chat-icon-white.svg"
Expand Down
23 changes: 21 additions & 2 deletions dpbr_front/app/src/lib/components/Toast.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
<script lang="ts">
import { toast } from "$lib/stores/toast";
import { fade, slide } from "svelte/transition";

let defaultToasts = $derived($toast.filter((t) => t.type !== "center"));
let centerToasts = $derived($toast.filter((t) => t.type === "center"));
</script>

<!-- Default bottom toasts -->
<div
class="fixed bottom-40 left-0 right-0 z-[100] flex flex-col items-center pointer-events-none gap-2 px-4"
>
{#each $toast as { id, message } (id)}
{#each defaultToasts as { id, message } (id)}
<div
in:slide={{ duration: 250 }}
out:fade={{ duration: 200 }}
class="bg-black/60 text-white px-6 py-2.5 rounded-3xl shadow-md text-[15px] font-medium text-center max-w-[90%] pointer-events-auto whitespace-nowrap"
class="bg-black/60 text-white px-6 py-2.5 rounded-3xl shadow-md text-[15px] font-medium text-center max-w-[90%] pointer-events-auto whitespace-pre-line"
>
{message}
</div>
{/each}
</div>

<!-- Centered toasts -->
<div
class="fixed inset-0 z-[101] flex flex-col items-center justify-center pointer-events-none gap-2 px-4"
>
{#each centerToasts as { id, message } (id)}
<div
in:slide={{ duration: 300, axis: "y" }}
out:fade={{ duration: 200 }}
class="bg-black/80 text-white px-8 py-6 rounded-3xl shadow-2xl text-[14px] font-medium text-center max-w-[90%] border border-white/20 pointer-events-auto whitespace-pre-line leading-relaxed"
>
{message}
</div>
Expand Down
5 changes: 3 additions & 2 deletions dpbr_front/app/src/lib/stores/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { writable } from 'svelte/store';
export interface ToastMessage {
id: number;
message: string;
type?: 'default' | 'center';
}

function createToastStore() {
Expand All @@ -12,9 +13,9 @@ function createToastStore() {

return {
subscribe,
show: (message: string, duration = 3000) => {
show: (message: string, duration = 3000, type: 'default' | 'center' = 'default') => {
const id = nextId++;
update((toasts) => [...toasts, { id, message }]);
update((toasts) => [...toasts, { id, message, type }]);

setTimeout(() => {
update((toasts) => toasts.filter((t) => t.id !== id));
Expand Down
47 changes: 40 additions & 7 deletions dpbr_front/app/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount, tick } from "svelte";
import Header from "$lib/components/Header.svelte";
import Sidebar from "$lib/components/Sidebar.svelte";
import CharacterCard from "$lib/components/CharacterCard.svelte";
import { getCharactersPaginated } from "$lib/api";
import type { Character } from "$lib/types";
import type { Snapshot } from "./$types";

let sidebarOpen = $state(false);
let characters = $state<Character[]>([]);
let loading = $state(true);
let loadingMore = $state(false);
let hasMore = $state(true);
let error = $state<string | null>(null);
let page = 1;
let page = $state(1);
const limit = 12;
let sentinel = $state<HTMLDivElement | null>(null);
let scrollContainer = $state<HTMLDivElement | null>(null);
let restoredScrollTop = 0;

export const snapshot: Snapshot = {
capture: () => ({
characters,
page,
hasMore,
scrollTop: scrollContainer?.scrollTop ?? 0,
}),
restore: (value) => {
characters = value.characters;
page = value.page;
hasMore = value.hasMore;
restoredScrollTop = value.scrollTop;
},
};

async function loadMoreCharacters() {
if (loadingMore || !hasMore) return;
Expand All @@ -28,7 +46,8 @@
characters = [...characters, ...result.items];
}

hasMore = characters.length < result.total && result.items.length > 0;
hasMore =
characters.length < result.total && result.items.length > 0;
if (result.items.length > 0) {
page += 1;
}
Expand All @@ -42,8 +61,16 @@
}
}

onMount(() => {
void loadMoreCharacters();
onMount(async () => {
if (characters.length === 0) {
void loadMoreCharacters();
} else {
loading = false;
if (scrollContainer && restoredScrollTop > 0) {
await tick();
scrollContainer.scrollTop = restoredScrollTop;
}
}
});

$effect(() => {
Expand Down Expand Up @@ -78,7 +105,10 @@
<Header variant="main" onMenuClick={() => (sidebarOpen = true)} />

<!-- Character Grid -->
<div class="flex-1 px-4 py-6 overflow-y-auto min-h-0">
<div
bind:this={scrollContainer}
class="flex-1 px-4 py-6 overflow-y-auto min-h-0"
>
{#if loading}
<div class="flex items-center justify-center h-full">
<p class="text-white">로딩 중...</p>
Expand All @@ -94,7 +124,10 @@
{/each}
</div>
{#if hasMore}
<div bind:this={sentinel} class="h-10 flex items-center justify-center">
<div
bind:this={sentinel}
class="h-10 flex items-center justify-center"
>
{#if loadingMore}
<p class="text-white text-sm">불러오는 중...</p>
{/if}
Expand Down
83 changes: 50 additions & 33 deletions dpbr_front/app/src/routes/member/[id]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { tick } from "svelte";
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import Header from "$lib/components/Header.svelte";
Expand All @@ -17,6 +18,7 @@
SettlementItem,
TeamMessageItem,
} from "$lib/types";
import type { Snapshot } from "./$types";

const ADMIN_TEAM_INFO = {
generation: "13기",
Expand Down Expand Up @@ -47,15 +49,53 @@
let settlements = $state<SettlementItem[]>([]);
let settlementsLoadingMore = $state(false);
let settlementsHasMore = $state(false);
let settlementsPage = 1;
let settlementsPage = $state(1);
const settlementsLimit = 10;
let settlementsSentinel = $state<HTMLDivElement | null>(null);
let teamMessages = $state<TeamMessageItem[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);

let scrollContainer = $state<HTMLDivElement | null>(null);
let restoredScrollTop = 0;
let restoredCharacterId = "";

export const snapshot: Snapshot = {
capture: () => ({
character,
settlements,
settlementsPage,
settlementsHasMore,
teamMessages,
scrollTop: scrollContainer?.scrollTop ?? 0,
charId: characterId,
}),
restore: (value) => {
character = value.character;
settlements = value.settlements;
settlementsPage = value.settlementsPage;
settlementsHasMore = value.settlementsHasMore;
teamMessages = value.teamMessages;
restoredScrollTop = value.scrollTop;
restoredCharacterId = value.charId;
},
};

$effect(() => {
// characterId가 변경될 때마다 데이터 로드
if (restoredCharacterId === characterId && character) {
loading = false;
if (scrollContainer && restoredScrollTop > 0) {
tick().then(() => {
if (scrollContainer) {
scrollContainer.scrollTop = restoredScrollTop;
}
});
}
// Reset restored ID after use to allow normal loads if ID changes later
restoredCharacterId = "";
return;
}
loadData();
});

Expand All @@ -76,21 +116,8 @@
} else {
character = fallbackAdminCharacter;
}
const members = await getTeamMembers();
const rolePriority: Record<string, number> = {
인사팀원: 1,
행사팀원: 2,
홍보팀원: 3,
인사팀장: 4,
행사팀장: 5,
홍보팀장: 6,
회장: 7,
};
teamMessages = members.sort(
(a, b) =>
(rolePriority[a.role] || 99) -
(rolePriority[b.role] || 99),
);
teamMessages = await getTeamMembers();

settlements = [];
settlementsHasMore = false;
return;
Expand All @@ -106,21 +133,8 @@
}

if (charData.name === ADMIN_TEAM_NAME) {
const members = await getTeamMembers();
const rolePriority: Record<string, number> = {
인사팀원: 1,
행사팀원: 2,
홍보팀원: 3,
인사팀장: 4,
행사팀장: 5,
홍보팀장: 6,
회장: 7,
};
teamMessages = members.sort(
(a, b) =>
(rolePriority[a.role] || 99) -
(rolePriority[b.role] || 99),
);
teamMessages = await getTeamMembers();

settlements = [];
settlementsHasMore = false;
} else {
Expand Down Expand Up @@ -214,10 +228,13 @@
<Header
variant="detail"
title={isAdminTeam ? "운영팀 한마디 상세" : "메생결산 상세"}
onBackClick={() => goto("/")}
onBackClick={() => history.back()}
/>

<div class="flex-1 overflow-y-auto flex flex-col gap-2 pb-8">
<div
bind:this={scrollContainer}
class="flex-1 overflow-y-auto flex flex-col gap-2 pb-8"
>
<!-- Character Info -->
<div class="flex items-center gap-4 bg-white px-6 py-5">
<div
Expand Down
Loading