Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@
### 예시
```
feat(#123) :: 로그인 기능 추가
```
```


15 changes: 15 additions & 0 deletions docs/hongin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### 작업 내역 (커밋 컨벤션 기반)

- feat :: middleware를 활용한 Protected Route 구현
- feat :: 로그인 미인증 사용자 접근 시 리다이렉트 처리 추가

- feat :: react-hot-toast를 활용한 사용자 알림(Toast) UI 적용
- refactor :: alert 기반 UX 제거 및 Toast 기반으로 개선

- feat :: 로그인 성공 후 redirect 파라미터 기반 경로 복귀 기능 구현

- refactor :: 하드코딩된 URL 제거 및 env 환경 변수로 전환
- chore :: .env.local 파일을 활용한 BASE_URL 설정

- feat :: Tailwind CSS 기반 로딩 스피너 UI 구현
- refactor :: 기존 텍스트 로딩 UI를 시각적 로딩 UI로 개선
69 changes: 69 additions & 0 deletions docs/imjemin수정.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 변경 사항 정리

## 수정된 파일 목록

- `src/components/layout/Header.tsx`
- `src/components/layout/Listbox.tsx`
- `src/components/ui/list.tsx`
- `src/app/(main)/[datas]/[id]/page.tsx`
- `src/util/makeDocument.tsx`

---

## 파일별 상세 변경 내용

### src/components/layout/Header.tsx

**수정**
- 데스크탑 헤더의 인증 로딩 중 표시되던 `로딩중...` 텍스트를 회전 스피너로 교체
- 모바일 메뉴의 인증 로딩 중 표시되던 `로딩중...` 텍스트를 회전 스피너로 교체
- 모바일 드롭다운 메뉴의 표시 방식을 `hidden` / `flex` 즉시 전환에서 `max-height` + `opacity` 기반 슬라이드 애니메이션(300ms)으로 변경

---

### src/components/layout/Listbox.tsx

**추가**
- `isLoading` prop 추가
- 로딩 중일 때 게시글 건수 영역을 스켈레톤(회색 블록)으로 표시
- 로딩 중일 때 데스크탑 테이블 행 6개를 pulse 스켈레톤으로 표시
- 로딩 중일 때 모바일 목록 행 6개를 pulse 스켈레톤으로 표시

**수정**
- 테이블 헤더(번호/제목/작성자/등록일/조회)를 로딩 상태와 무관하게 항상 렌더링하도록 구조 변경

**삭제**
- 더 이상 사용되지 않는 `Loading` 컴포넌트 import 제거
- 도달 불가능한 `<Loading />` fallback 분기 제거

---

### src/components/ui/list.tsx

**추가**
- `isLoading` 상태 추가 (초기값 `true`)
- Firebase 데이터 fetch 완료 후 `finally` 블록에서 `isLoading`을 `false`로 전환
- `Listbox`에 `isLoading` prop 전달

---

### src/app/(main)/[datas]/[id]/page.tsx

**추가**
- `imageLoaded` 상태 추가
- 활동 상세 이미지에 `loading="lazy"` 속성 추가
- 이미지 로드 완료 시 `onLoad`로 `imageLoaded`를 `true`로 전환
- 이미지 로드 전후로 `opacity-0` -> `opacity-100` fade-in 전환 효과(500ms) 추가

**수정**
- 이미지 컨테이너 배경색 `bg-gray-50` -> `bg-gray-100` 변경 (로딩 중 플레이스홀더 시각화)

---

### src/util/makeDocument.tsx

**추가**
- 게시물 본문 인라인 이미지(`next/image`)에 `loading="lazy"` 속성 추가

**수정**
- 인라인 주석(`{/* 원하는 높이 설정 */}`) 제거
Empty file added docs/분석.md
Empty file.
29 changes: 29 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;

const { pathname } = request.nextUrl;

const protectedPaths = ["/write"];

const isProtected = protectedPaths.some((path) =>
pathname.startsWith(path)
);

if (isProtected && !token) {

const loginUrl = new URL("/login/success", request.url);

loginUrl.searchParams.set("redirect", pathname);

return NextResponse.redirect(loginUrl);
}

return NextResponse.next();
}

export const config = {
matcher: ["/write/:path*"],
};
28 changes: 27 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"nodemailer": "^7.0.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.6.0",
"zustand": "^5.0.5"
},
"devDependencies": {
Expand Down
11 changes: 9 additions & 2 deletions src/app/(main)/[datas]/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CommentType = {
export default function DetailPage() {
const [commentText, setCommentText] = useState('');
const [role, setRole] = useState<string | null>(null);
const [imageLoaded, setImageLoaded] = useState(false);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [item, setItem] = useState<any>(null);
Expand Down Expand Up @@ -181,9 +182,15 @@ export default function DetailPage() {
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
{item.image && (
<div className="w-full lg:w-2/5">
<div className="aspect-square w-full overflow-hidden rounded-xl bg-gray-50">
<div className="aspect-square w-full overflow-hidden rounded-xl bg-gray-100">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={item.image} alt="이미지" className="h-full w-full object-cover" />
<img
src={item.image}
alt="이미지"
loading="lazy"
onLoad={() => setImageLoaded(true)}
className={`h-full w-full object-cover transition-opacity duration-500 ${imageLoaded ? "opacity-100" : "opacity-0"}`}
/>
</div>
</div>
)}
Expand Down
2 changes: 0 additions & 2 deletions src/app/(main)/signupform/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ export default function SignupForm() {
let message = error?.message || "회원가입 중 오류가 발생했습니다.";
if (error?.code === "auth/email-already-in-use") {
message = "이미 가입된 이메일입니다.";
alert(message);
router.push("/login");
return;
} else if (error?.code === "auth/weak-password") {
message = "비밀번호가 너무 약합니다. 6자 이상으로 설정해주세요.";
Expand Down
25 changes: 16 additions & 9 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import "./globals.css";
import FirebaseAnalytics from "@/components/FirebaseAnalytics";
import { AuthProvider } from "@/components/providers/AuthProvider";

import { Toaster } from "react-hot-toast";

const baseUrl = process.env.NEXT_PUBLIC_BASE_URL!;

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
Expand All @@ -15,11 +19,10 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});

// app/layout.tsx
export const metadata: Metadata = {
title: {
default: "NDIE",
template: "%s | NDIE", // 각 페이지 제목 뒤에 붙는 기본 이름
template: "%s | NDIE",
},
description:
"사단법인 디지털과포용성네트워크(NDIE)는 디지털 전환 속 소외된 이들을 위한 포용적 디지털 사회 구현을 목표로 연구, 교육, 정책 제안, 네트워킹 등 다양한 활동을 수행합니다.",
Expand All @@ -35,18 +38,18 @@ export const metadata: Metadata = {
authors: [
{
name: "사단법인 디지털과포용성네트워크",
url: "https://ndie-fe-985895714915.asia-northeast1.run.app/", // 실제 도메인으로 바꿔주세요
url: baseUrl,
},
],
openGraph: {
title: "디지털과포용성네트워크",
description:
"디지털 포용성과 다양성을 추구하는 비영리 법인 NDIE. 디지털 접근성 향상과 인간 중심의 기술 환경 조성을 위한 연구와 활동을 수행합니다.",
url: "https://ndie-fe-985895714915.asia-northeast1.run.app/", // 실제 도메인으로 바꿔주세요
url: baseUrl,
siteName: "디지털과포용성네트워크",
images: [
{
url: "https://storage.googleapis.com/focus-pathfinder/e675f822-6cdb-4e11-9d02-d94b102cb7ce", // 사이트 og 이미지 URL
url: "https://storage.googleapis.com/focus-pathfinder/e675f822-6cdb-4e11-9d02-d94b102cb7ce",
width: 1200,
height: 630,
},
Expand All @@ -58,12 +61,14 @@ export const metadata: Metadata = {
title: "디지털과포용성네트워크",
description:
"포용적 디지털 사회를 위한 연구·교육·정책 활동. 디지털 소외 해소와 사회 참여 기회 확대를 위해 노력합니다.",
images: ["https://storage.googleapis.com/focus-pathfinder/e675f822-6cdb-4e11-9d02-d94b102cb7ce"],
images: [
"https://storage.googleapis.com/focus-pathfinder/e675f822-6cdb-4e11-9d02-d94b102cb7ce",
],
},
icons: {
icon: '/favicon.ico',
icon: "/favicon.ico",
},
metadataBase: new URL("https://ndie-fe-985895714915.asia-northeast1.run.app/"),
metadataBase: new URL(baseUrl),
};

export default function RootLayout({
Expand All @@ -78,7 +83,9 @@ export default function RootLayout({
>
<FirebaseAnalytics />
<AuthProvider>{children}</AuthProvider>

<Toaster position="top-center" />
</body>
</html>
);
}
}
10 changes: 7 additions & 3 deletions src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const Header = () => {

<div className="hidden items-center gap-6 md:flex">
{showLoading ? (
<span className="text-gray-400">로딩중...</span>
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-gray-500" />
) : isLoggedIn ? (
<button onClick={handleLogout} className="cursor-pointer text-sm font-medium">
로그아웃
Expand Down Expand Up @@ -101,8 +101,11 @@ export const Header = () => {
</div>

<div
className={`md:hidden ${isMenuOpen ? "flex" : "hidden"} flex-col gap-4 border-t border-[#EAEAEA] bg-white px-4 py-4 shadow-sm`}
className={`md:hidden overflow-hidden transition-all duration-300 ease-in-out ${
isMenuOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
}`}
>
<div className="flex flex-col gap-4 border-t border-[#EAEAEA] bg-white px-4 py-4 shadow-sm">
<nav className="flex flex-col gap-3 text-sm">
{navLinks.map(({ href, label }) => (
<Link
Expand All @@ -127,7 +130,7 @@ export const Header = () => {
</nav>
<div className="border-t border-[#EAEAEA] pt-3">
{showLoading ? (
<span className="text-gray-400">로딩중...</span>
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-gray-500" />
) : isLoggedIn ? (
<button onClick={logoutAndClose} className="text-sm font-medium">
로그아웃
Expand All @@ -144,6 +147,7 @@ export const Header = () => {
)}
</div>
</div>
</div>
</header>
);
};
Loading