Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a47fb14
[fix] 챗봇 추천 페이지 수정 및 기능추가 (#130)
ahk0413 Oct 13, 2025
61bd59c
Design/main#11 (#131)
EunbinJung Oct 13, 2025
f071906
Style/main page 2#122 (#132)
mtm-git1018 Oct 13, 2025
bd767b0
Style/메인페이지 슬라이드영역 (#135)
mtm-git1018 Oct 14, 2025
1dbeb90
[fix] 로그아웃 시 auth/me api 자동호출로 401 에러 (#136)
ahk0413 Oct 14, 2025
993820d
Design/main#11 (#138)
EunbinJung Oct 15, 2025
d6035f4
[fix] 레이아웃 분리 (#139)
ahk0413 Oct 15, 2025
0a71cd0
Refactor/recipe fetch (#140)
mtm-git1018 Oct 15, 2025
8ee022c
Merge remote-tracking branch 'origin/main' into dev
mtm-git1018 Oct 15, 2025
95248c2
[fix] 경로 오류 수정
mtm-git1018 Oct 15, 2025
a160b2a
경로 수정
mtm-git1018 Oct 15, 2025
9c64186
[fix] 경로수정
mtm-git1018 Oct 15, 2025
52e82c3
Feat/write#19 (#142)
EunbinJung Oct 15, 2025
3f0e6f3
[fix] MainSlide 수정 (#143)
ahk0413 Oct 15, 2025
1d0e476
Refactor/칵테일 정렬 기능 수정 (#144)
mtm-git1018 Oct 15, 2025
0d6c6be
Merge remote-tracking branch 'origin/main' into dev
mtm-git1018 Oct 15, 2025
c3c3644
[fix]충돌에러수정
mtm-git1018 Oct 15, 2025
d28a1f4
[style] 폰트 추가
mtm-git1018 Oct 15, 2025
94a65e0
[fix]파일 내 코드중복 수정
mtm-git1018 Oct 15, 2025
9e976a5
[chore]포매팅
mtm-git1018 Oct 15, 2025
06e809c
[fix]타입중복 수정
mtm-git1018 Oct 15, 2025
fcef37f
[chore]포매팅
mtm-git1018 Oct 15, 2025
ca5539a
docs/ 폰트 추가 및 삭제 (#146)
mtm-git1018 Oct 15, 2025
1c30893
[fix] scroll 위치이동
ahk0413 Oct 15, 2025
b517217
Merge remote-tracking branch 'origin/dev' into dev
ahk0413 Oct 15, 2025
ad53efa
Feat/write#19 (#147)
EunbinJung Oct 15, 2025
13fd5e1
Merge remote-tracking branch 'origin/dev' into dev
ahk0413 Oct 15, 2025
b75f7e8
[style] 폰트 추가
mtm-git1018 Oct 15, 2025
0efeda3
Merge branch 'dev' of https://github.com/prgrms-web-devcourse-final-p…
mtm-git1018 Oct 15, 2025
82254e5
[feat] 스크롤 버튼 추가
ahk0413 Oct 15, 2025
d02add2
Merge remote-tracking branch 'origin/dev' into dev
ahk0413 Oct 15, 2025
3723a9b
[fix] 시작 애니메이션 원복
ahk0413 Oct 15, 2025
c99fd87
[docs]README
mtm-git1018 Oct 15, 2025
49a5fbd
Feat/write#19 (#148)
EunbinJung Oct 15, 2025
85690d2
Merge branch 'main' into dev
EunbinJung Oct 15, 2025
c4b7b8b
[fix] 로그인 시 여러번 뜨는 toast 알림 이슈 수정
ahk0413 Oct 15, 2025
b3ceb95
Merge branch 'dev' of https://github.com/prgrms-web-devcourse-final-p…
ahk0413 Oct 15, 2025
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
169 changes: 169 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# SSOUL 프로젝트 인수인계 문서

## �� 프로젝트 개요

**프로젝트명**: SSOUL (칵테일을 좋아하는 사람들을 위한 서비스)
**기술 스택**: Next.js 15, React 19, TypeScript, Tailwind CSS
**저장소**: https://github.com/prgrms-web-devcourse-final-project/WEB5_6_HaeDokCoding_FE

## �� 시작하기

### 개발 환경 설정
```bash
# 의존성 설치
npm install

# 개발 서버 실행
npm run dev

# 빌드
npm run build

# 코드 포맷팅
npm run format

# 린트 검사
npm run lint
```

### 환경 변수
프로젝트는 개발/운영 환경에 따라 다른 API URL을 사용합니다:
- `NEXT_PUBLIC_API_URL_DEV`: 개발 환경 API URL
- `NEXT_PUBLIC_API_URL_PROD`: 운영 환경 API URL

## �� 프로젝트 구조

```
src/
├── app/ # Next.js App Router 페이지
│ ├── layout.tsx # 루트 레이아웃
│ ├── page.tsx # 메인 페이지
│ ├── community/ # 커뮤니티 관련 페이지
│ ├── recipe/ # 칵테일 레시피 페이지
│ ├── recommend/ # 취향 추천 페이지
│ ├── login/ # 로그인 관련 페이지
│ ├── mypage/ # 마이페이지
│ └── api/ # API 설정
├── domains/ # 도메인별 컴포넌트
│ ├── community/ # 커뮤니티 도메인
│ ├── recipe/ # 레시피 도메인
│ ├── recommend/ # 추천 도메인
│ ├── login/ # 로그인 도메인
│ ├── mypage/ # 마이페이지 도메인
│ └── shared/ # 공통 도메인 컴포넌트
└── shared/ # 공통 컴포넌트 및 유틸
├── components/ # 공통 UI 컴포넌트
├── styles/ # 글로벌 스타일
├── assets/ # 이미지, 아이콘 등
├── hook/ # 공통 훅
├── types/ # 타입 정의
└── utills/ # 유틸리티 함수
```

## �� 주요 기능

### 1. 인증 시스템
- **소셜 로그인**: Google, Kakao, Naver 지원
- **상태 관리**: Zustand + localStorage persist
- **주요 파일**:
- `src/domains/shared/store/auth.ts`: 인증 상태 관리
- `src/domains/login/hook/useAuthHooks.ts`: 로그인 관련 훅
- `src/app/api/config/appConfig.ts`: API 설정

### 2. 페이지별 기능

#### �� 메인 페이지 (`/`)
- 현재 기본 구조만 구현됨
- 추후 확장 예정

#### �� 칵테일 레시피 (`/recipe`)
- **주요 컴포넌트**:
- `CocktailList`: 칵테일 목록 표시
- `Accordion`: 필터링 옵션
- `SelectBox`: 정렬 옵션
- **기능**: 검색, 필터링, 정렬
- **상세 페이지**: `/recipe/[id]` - 개별 칵테일 상세 정보

#### �� 커뮤니티 (`/community`)
- **주요 컴포넌트**:
- `PostCard`: 게시물 카드
- `CommunityTab`: 카테고리 탭
- `WriteBtn`: 글쓰기 버튼
- **카테고리**: 레시피, 팁, 질문, 자유
- **글쓰기**: `/community/write`
- **상세 페이지**: `/community/[id]`

#### �� 취향 추천 (`/recommend`)
- **챗봇 기반 추천**: `ChatSection` 컴포넌트
- **주요 컴포넌트**:
- `BotMessage`, `UserMessage`: 메시지 컴포넌트
- `BotOptions`: 선택 옵션
- `MessageInput`: 입력창
- `TypingIndicator`: 타이핑 효과

#### �� 마이페이지 (`/mypage`)
- **기본 리다이렉트**: `/mypage` → `/mypage/mybar`
- **주요 섹션**:
- `/mypage/mybar`: 나만의 바
- `/mypage/my-active`: 활동 내역 (게시물, 댓글, 좋아요)
- `/mypage/my-alarm`: 알림 설정
- `/mypage/my-setting`: 계정 설정

#### �� 로그인 (`/login`)
- **소셜 로그인**: `SocialLogin` 컴포넌트
- **성공 페이지**: `/login/success`
- **신규 사용자**: `/login/user/first-user`

## �� 기술적 세부사항

### 상태 관리
- **Zustand**: 클라이언트 상태 관리
- **Persist**: localStorage를 통한 상태 영속화
- **주요 스토어**:
- `auth.ts`: 사용자 인증 상태
- `accordionStore.ts`: 아코디언 UI 상태

### UI/UX
- **Tailwind CSS**: 스타일링
- **React Hot Toast**: 토스트 알림
- **Lottie**: 로딩 애니메이션
- **GSAP**: 고급 애니메이션
- **Responsive**: 모바일/데스크톱 대응

### 개발 도구
- **ESLint**: 코드 품질 관리
- **Prettier**: 코드 포맷팅
- **Husky**: Git hooks
- **Lint-staged**: 커밋 전 검사

## �� 주요 설정 파일

- `next.config.ts`: Next.js 설정
- `tailwind.config.js`: Tailwind CSS 설정
- `eslint.config.mjs`: ESLint 설정
- `tsconfig.json`: TypeScript 설정

## �� 반응형 디자인

프로젝트는 모바일 우선(Mobile-first) 접근 방식을 사용합니다:
- **모바일**: 기본 스타일
- **태블릿**: `md:` prefix
- **데스크톱**: `lg:`, `xl:` prefix

## �� 주의사항

1. **환경 변수**: 개발/운영 환경에 맞는 API URL 설정 필요
2. **인증 토큰**: localStorage에 저장되므로 보안 고려 필요
3. **API 통신**: `credentials: 'include'` 설정으로 쿠키 기반 인증
4. **Git Hooks**: Husky 설정으로 커밋 전 자동 검사

## �� 추가 문의

- **저장소 이슈**: https://github.com/prgrms-web-devcourse-final-project/WEB5_6_HaeDokCoding_FE/issues
- **주요 브랜치**: `main` (메인), `dev` (개발)

---

**작성일**: 2025-10-14
**작성자**: 이성헌
**버전**: 1.0
Binary file added public/fonts/NanumSquareNeo-aLt.ttf
Binary file not shown.
Binary file added public/fonts/NanumSquareNeo-bRg.ttf
Binary file not shown.
Binary file added public/fonts/NanumSquareNeo-cBd.ttf
Binary file not shown.
Binary file added public/fonts/NanumSquareNeo-dEb.ttf
Binary file not shown.
Binary file added public/fonts/NanumSquareNeo-eHv.ttf
Binary file not shown.
7 changes: 4 additions & 3 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import FooterWrapper from '@/shared/components/footer/FooterWrapper';
import Header from '@/shared/components/header/Header';

function NoLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Header className="bg-transparent w-full h-[44px] md:h-[60px] flex items-center justify-between px-[12px] fixed top-0 left-0 z-50 transition-transform duration-200 ease-in-ou" />
<Header
isMain={true}
className="w-full h-[44px] md:h-[60px] flex items-center justify-between px-[12px] fixed top-0 left-0 z-50 transition-transform duration-200 ease-in-ou"
/>
<main className="flex flex-1">{children}</main>
<FooterWrapper />
</>
);
}
Expand Down
3 changes: 1 addition & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import '@/shared/styles/global.css';
import type { Metadata } from 'next';
import '@/shared/styles/global.css';
import { Toaster } from 'react-hot-toast';
import Header from '@/shared/components/header/Header';
import FooterWrapper from '@/shared/components/footer/FooterWrapper';
import ScrollTopBtnWrapper from '@/shared/components/scroll-top/ScrollTopBtnWrapper';
import KaKaoScript from './api/kakao/KaKaoScript';
import 'swiper/css';
Expand Down
6 changes: 4 additions & 2 deletions src/domains/login/hook/useLoginRedirect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useAuthStore } from '@/domains/shared/store/auth';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { getCookie, removeCookie } from '@/domains/shared/auth/utils/cookie';
import { useToast } from '@/shared/hook/useToast';
Expand All @@ -12,6 +12,7 @@ export const useLoginRedirect = () => {

const [loading, setLoading] = useState(true);
const [welcomeModalOpen, setWelcomeModalOpen] = useState(false);
const hasShownToast = useRef(false);

useEffect(() => {
if (!user && loading) {
Expand Down Expand Up @@ -39,7 +40,8 @@ export const useLoginRedirect = () => {

if (pathname.startsWith('/login/user/first-user')) {
setWelcomeModalOpen(true);
} else if (pathname.startsWith('/login/user/success')) {
} else if (pathname.startsWith('/login/user/success') && !hasShownToast.current) {
hasShownToast.current = true;
toastSuccess(`${user.nickname}님 \n 로그인 성공 🎉`);
router.replace(preLoginPath);
setTimeout(() => removeCookie('preLoginPath'), 500);
Expand Down
29 changes: 19 additions & 10 deletions src/domains/main/cocktailDrop/CocktailDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,23 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) {
);

// 로고 위에서 아래로 자연스럽게 등장
const screenWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const isTablet = screenWidth >= 640 && screenWidth < 1024;
const isMobile = screenWidth < 640;

// 뷰포트 높이 기반으로 로고 위치 계산
const logoFinalY = isMobile
? `-${viewportHeight * 0.3}px`
: isTablet
? `-${viewportHeight * -0.8}px`
: '0px';

gsap.fromTo(
logoRef.current,
{ y: -300, opacity: 0 },
{
y: !isDesktop ? -230 : -18, // 데스크톱이 아닐 때 더 위로
y: logoFinalY, // 뷰포트 높이 기반 계산
opacity: 1,
duration: 3,
ease: 'power3.out',
Expand All @@ -69,7 +81,7 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) {
return (
<div
ref={containerRef}
className="relative w-full lg:min-h-[110vh] min-h-[89vh] flex flex-col md:justify-center justify-end items-center mt-10 overflow-hidden"
className="relative w-full lg:min-h-[120vh] md:min-h-[95vh] min-h-[87vh] flex flex-col lg:justify-center md:justify-center justify-end items-center mt-10 overflow-hidden"
id="scroll-fixed"
>
{/* 대각선 줄 1 */}
Expand All @@ -84,7 +96,7 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) {
/>

{/* 로고 */}
<div ref={logoRef} className="absolute md:w-115 w-85 md:h-90 h-40">
<div ref={logoRef} className="absolute z-4 md:w-115 w-65 md:h-90 h-40">
<Image
src="/logo.svg"
alt="로고 이미지"
Expand All @@ -95,20 +107,17 @@ function CocktailDrop({ isDesktop = false }: CocktailDropProps) {
/>
</div>

<div className="w-full md:h-90 h-30"></div>

{/* 컵 이미지 - 모바일에서 바닥에 붙도록 */}
<div className="md:relative absolute bottom-0">
<div className="z-5 absolute bottom-0">
<Image
src={Cocktailcup}
alt="칵테일 컵"
width={900}
height={700}
priority
className="md:w-auto md:h-auto w-[500px] h-[400px] object-cover"
style={{ height: 'auto' }}
className="md:w-[700px] w-[500px] object-contain"
/>
</div>
<div className="absolute md:bottom-35 bottom-20 flex items-center justify-center z-3 w-full">
<div className="absolute md:bottom-35 bottom-20 flex items-center justify-center z-10 w-full">
<PassBtn />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/domains/main/components/3d/HomeLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Image from 'next/image';
function HomeLogo({ isDesktop }: { isDesktop: boolean }) {
return (
<div
className="z-5 absolute md:top-8 md:left-10 md:translate-none top-13 left-1/2 -translate-x-1/2"
style={{ width: !isDesktop ? 400 : 700, height: !isDesktop ? 70 : 240 }}
className="z-5 absolute md:top-22 md:left-10 md:translate-none top-30 left-1/2 -translate-x-1/2"
style={{ width: !isDesktop ? 400 : 580, height: !isDesktop ? 70 : 210 }}
>
<Image src={'/logo.svg'} alt="로고 이미지" fill priority className="object-contain" />
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/domains/main/components/3d/HomeModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ function Model({ onLoaded }: Props) {
return (
<primitive
object={scene}
scale={5.8}
position={[0, -1.2, 0]}
scale={4.6}
position={[0, -0.6, 0]}
rotation={[-0.15, Math.PI + 3, 0]}
/>
);
Expand Down
5 changes: 3 additions & 2 deletions src/domains/main/components/3d/HomeText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ function HomeText({ isDesktop }: { isDesktop: boolean }) {
return (
<>
{!isDesktop ? (
<p className="absolute top-32 text-sm left-1/2 -translate-x-1/2 whitespace-nowrap">
어떤 칵테일이 끌리시나요? SSoul이 쉽게 골라드릴게요.
<p className="absolute top-48 text-sm left-1/2 -translate-x-1/2 text-center mt-4">
어떤 칵테일이 끌리시나요? <br />
SSoul이 쉽게 골라드릴게요.
</p>
) : (
<p className="absolute bottom-45 right-12 font-serif text-xl text-right font-normal z-20">
Expand Down
2 changes: 0 additions & 2 deletions src/domains/main/components/3d/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import HomeModel from './HomeModel';
import HomeLogo from './HomeLogo';
import HomeText from './HomeText';
import Scroll from './Scroll';
import { useEffect, useState } from 'react';
import ModelImage from './ModelImage';

Expand All @@ -30,7 +29,6 @@ function Landing({ setIsLoading, isDesktop }: Props) {
<>
<HomeLogo isDesktop={isDesktop} />
<HomeText isDesktop={isDesktop} />
<Scroll isDesktop={isDesktop} />
</>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/domains/main/components/3d/ModelImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function ModelImage({ onLoaded }: Props) {
width={260}
height={290}
priority
className="object-cover w-[300px] h-[390px]"
className="object-cover w-[300px] h-[350px]"
/>
</div>
</div>
Expand Down
38 changes: 23 additions & 15 deletions src/domains/main/components/3d/Scroll.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import Lottie from 'lottie-react';
import scroll from '@/shared/assets/lottie/ScrollDownAnimation.json';

function Scroll({ isDesktop }: { isDesktop: boolean }) {
const style = !isDesktop
? {
width: 45,
height: 45,
}
: {
width: 60,
height: 60,
};
type Props = {
ref: React.RefObject<HTMLButtonElement | null>;
};

function Scroll({ ref }: Props) {
const style = {
width: 50,
height: 50,
};
return (
<div className="absolute bottom-18 left-1/2 -translate-x-1/2 rounded-full md:bg-secondary/10 bg-primary/30 z-11 md:w-[60px] md:h-[60px] w-[45px] h-[45px]">
<div className="z-11">
<Lottie animationData={scroll} style={style} aria-hidden loop={true} />
</div>
</div>
<button
ref={ref}
type="button"
onClick={() => {
window.scrollBy({
top: 1000,
behavior: 'smooth',
});
}}
aria-label="아래로 스크롤"
className="fixed bottom-18 left-1/2 -translate-x-1/2 rounded-full bg-[#000000]/70 z-11 md:w-[60px] md:h-[60px] flex-center cursor-pointer"
>
<Lottie animationData={scroll} style={style} aria-hidden loop={true} />
</button>
);
}

Expand Down
Loading