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
162 changes: 122 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,132 @@
# React + TypeScript + Vite
# [💌 WOOGYEOL: 우리 결혼해요](https://woogyeol.site/)

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
> 개발 기간: 2025.01 ~ 2025.07

Currently, two official plugins are available:
"우리 결혼해요"는 💌 신랑 신부가 직접 꾸미는 모바일 청첩장을 손쉽게 제작할 수 있도록 돕는 플랫폼입니다. <br/>
📆 일정과 📍 위치 정보를 제공하고, 하객은 📊 참석 여부를 등록하며, 📸 실시간 포토월에 축하 사진과 메시지를 남길 수 있어 모두가 함께 추억을 만들어 갑니다. <br/>
하객과의 소통을 자연스럽고 따뜻하게 이어주는, 우리만의 특별한 초대장 서비스입니다.

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
<br/>

## Expanding the ESLint configuration
- [🔗 서비스 링크](https://woogyeol.site/)
- [📚 노션](https://www.notion.so/19e9673ec79780a3b17bed3825f5fa8c?pvs=21)
- [🎨 피그마](https://www.figma.com/design/Amij7OxsmnsATHkYM5PO52/Woo-Gyeol?node-id=3-62788&t=WoMAwjY7bvNK1etC-1)

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
<br/>

- Configure the top-level `parserOptions` property like this:
## 🔑 주요 기능

### **청첩장 관리**

> 나만의 청첩장을 생성, 수정, 조회, 삭제할 수 있습니다.

- 생성 / 수정 / 삭제 / 조회 기능
- 내 청첩장 목록 확인

### **청첩장 만들기: 3단계 입력과 8가지 선택 기능**

> 3단계의 입력 과정을 거쳐 청첩장을 만들 수 있습니다. (기본 정보 입력 -> 기능 선택 -> 테마 선택) <br/>

- 캘린더
- 지도/교통수단
- 갤러리
- 축의금
- 연락하기
- 공지사항
- 글꼴
- 배경음악

### **청첩장 공유**

> 제작한 청첩장을 하객들에게 공유할 수 있습니다.

- URL 복사
- 카카오톡 공유
- QR 코드 저장

### **RSVP: 참석여부 통계**

> 하객들로부터 참석여부 데이터를 받고 이를 관리할 수 있습니다. <br/>

- 참석/불참 통계 시각화
- 상세 입력 내역 조회
- 엑셀 다운로드 (.xlsx)

### **포토톡: 실시간 포토월**

> 하객들로부터 실시간 포토월 데이터를 받을 수 있습니다.

- 사진 + 축하 메시지 업로드
- 하객 이미지 다운로드 / 삭제 기능 지원
- 관리자 권한 처리

### **다크 모드 지원**

> 다크 모드를 통해 청첩장의 색감 반전을 경험할 수 있습니다.

<br/>

## 🛠️ 기술 스택

| 구분 | 기술 |
| ----------------- | ---------------------- |
| **Frontend** | React, TypeScript |
| **스타일** | TailwindCSS, Storybook |
| **상태관리** | React Query, Zustand |
| **번들러** | Vite |
| **테스트** | Jest |
| **배포** | Vercel |
| **패키지 매니저** | npm |

<br/>

## 📁 폴더 구조

```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
});
```
src
┣ components # 공통/기능별 UI 컴포넌트
┣ common # 재사용 가능한 컴포넌트
┣ form # 청첩장 정보 입력 관련 UI
┣ display # 완성된 청첩장 관련 UI
┣ phototalk # 포토톡 관련 컴포넌트
┣ mypage # 내정보 관련 컴포넌트
┣ assets
┣ constants # 정적 데이터
┣ hooks # 커스텀 훅
┣ pages # 라우팅 기반 페이지 컴포넌트
┣ services # api 서비스 모듈
┣ store # Zustand 전역 상태 저장소
┣ types # 전역 타입 정의
┣ utils # 유틸 함수
┣ styles # 전역 스타일
┣ App.tsx # 라우터 및 전체 레이아웃 구성

- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:

```js
// eslint.config.js
import react from 'eslint-plugin-react';

export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
});
```

<br/>

## 🤙 커밋 컨벤션

| 태그 | 설명 |
| ---------- | ------------------------------------- |
| `feat` | 새로운 기능 추가 |
| `fix` | 버그 수정 |
| `style` | 코드 포맷, 세미콜론 등 변경 |
| `refactor` | 코드 리팩토링 |
| `test` | 테스트 코드 작성 |
| `docs` | 문서 수정 |
| `chore` | 빌드 업무 수정, 패키지 매니저 설정 등 |
| `ci` | CI 관련 설정 |
| `build` | 빌드 파일 관련 |
| `revert` | 커밋 되돌리기 |

<br/>

## 👥 멤버 소개

| FE | FE | FE | FE |
| :------------------------------------------------------------------: | :-------------------------------------------------------------------: | :-----------------------------------------------------------------: | :------------------------------------------------------------------: |
| <img src="https://github.com/eesoyeon.png" width="100" height="100"> | <img src="https://github.com/meteorqz6.png" width="100" height="100"> | <img src="https://github.com/chaeon1.png" width="100" height="100"> | <img src="https://github.com/nowrobin.png" width="100" height="100"> |
| [이소연](https://github.com/eesoyeon) | [남유성](https://github.com/meteorqz6) | [황채연](https://github.com/chaeon1) | [한정욱](https://github.com/nowrobin) |

<br/>
102 changes: 49 additions & 53 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,69 +20,65 @@ import PreviewPhotoTalkPage from '@/pages/PhotoTalk/PreviewPhotoTalkPage';
import PreviewInvitationPage from '@/pages/PreviewInvitationPage';
import ResultPage from '@/pages/ResultPage';
import GuestPhotoTalkPage from '@/pages/PhotoTalk/GuestPhotoTalkPage';
import { useUserStore } from './store/useUserStore';
import useAuthStore from './store/useAuthStore';
import { useEffect } from 'react';
import Authorized from '@/components/common/Authorized/Authorized';
import BasicInformationPage from './pages/BasicInformationPage';

function App() {
const queryClient = new QueryClient();

const fetchUserInfo = useUserStore((state) => state.fetchUserInfo);
const token = useAuthStore((state) => state.accessToken);

useEffect(() => {
if (token) {
fetchUserInfo();
}
}, [token]);

return (
<DarkModeProvider>
<QueryClientProvider client={queryClient}>
<ScrollToTop />
<Routes>
{/* <Route path="/" element={<Navigate to="/" replace />} /> */}
<Route path={'/'} element={<StartPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path={'/reset-password'} element={<ResetPasswordPage />} />
<Route path={'/signup'} element={<SignUpPage />} />
<Authorized>
<ScrollToTop />
<Routes>
{/* <Route path="/" element={<Navigate to="/" replace />} /> */}
<Route path={'/'} element={<StartPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path={'/reset-password'} element={<ResetPasswordPage />} />
<Route path={'/signup'} element={<SignUpPage />} />

{/* 청첩장 만들기 */}
<Route path={'/dashboard'} element={<DashBoardPage />} />
<Route path={'/create/:id'} element={<CreateInvitationPage />} />
<Route path={'/edit/:id'} element={<CreateInvitationPage />} />
<Route
path={'/preview/:userId?/:invitationId?'}
element={<PreviewInvitationPage />}
/>
<Route
path={'/preview/phototalk'}
element={<PreviewPhotoTalkPage />}
/>
{/* 청첩장 만들기 */}
<Route path={'/dashboard'} element={<DashBoardPage />} />
<Route path={'/create/:id'} element={<CreateInvitationPage />} />
<Route path={'/edit/:id'} element={<CreateInvitationPage />} />
<Route
path={'/preview/:userId?/:invitationId?'}
element={<PreviewInvitationPage />}
/>
<Route
path={'/preview/phototalk'}
element={<PreviewPhotoTalkPage />}
/>

{/* 청첩장 완성본 */}
<Route
path={'/result/:userId/:invitationId'}
element={<ResultPage />}
/>
<Route
path={'/phototalk/:userId/:invitationId'}
element={<GuestPhotoTalkPage />}
/>
{/* 청첩장 완성본 */}
<Route
path={'/result/:userId/:invitationId'}
element={<ResultPage />}
/>
<Route
path={'/phototalk/:userId/:invitationId'}
element={<GuestPhotoTalkPage />}
/>

{/* 마이페이지 */}
<Route path={'/mypage/rsvp'} element={<MyPage />} />
<Route path={'/mypage/phototalk'} element={<MyPage />} />
<Route path={'/mypage/edit'} element={<EditProfilePage />} />
<Route
path={'/mypage/edit/password'}
element={<ChangePasswordPage />}
/>
<Route path="/oauth/callback/kakao" element={<KakaoRedirect />} />
<Route path="/oauth/callback/naver" element={<NaverRedirect />} />
<Route path={'*'} element={<NotFound404 />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
{/* 마이페이지 */}
<Route path={'/mypage/rsvp'} element={<MyPage />} />
<Route path={'/mypage/phototalk'} element={<MyPage />} />
<Route path={'/mypage/edit'} element={<EditProfilePage />} />
<Route
path={'/mypage/edit/password'}
element={<ChangePasswordPage />}
/>
<Route
path={'/mypage/edit/basic-information'}
element={<BasicInformationPage />}
/>
<Route path="/oauth/callback/kakao" element={<KakaoRedirect />} />
<Route path="/oauth/callback/naver" element={<NaverRedirect />} />
<Route path={'*'} element={<NotFound404 />} />
</Routes>
<ReactQueryDevtools initialIsOpen={false} />
</Authorized>
</QueryClientProvider>
</DarkModeProvider>
);
Expand Down
34 changes: 28 additions & 6 deletions src/actions/invitationAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import fonts from '@/constants/fonts';
const defaultCoord = { lat: 37.5086, lng: 127.0397 };
// const today = new Date();

export const defaultInvitationValues: Omit<InvitationDetail, 'title'> = {
createdAt: '',
export const defaultInvitationValues: Omit<
InvitationDetail,
'title' | 'createdAt'
> = {
groomName: '',
brideName: '',
date: [null, null, null],
Expand Down Expand Up @@ -157,7 +159,7 @@ export const defaultInvitationValues: Omit<InvitationDetail, 'title'> = {

export const getInvitationAction = (): Omit<
InvitationDetail,
'imgUrl' | 'galleries' | 'notices' | 'title'
'imgUrl' | 'galleries' | 'notices' | 'title' | 'createdAt'
> => {
//웨딩 정보
const {
Expand All @@ -168,7 +170,7 @@ export const getInvitationAction = (): Omit<
weddingHallDetail,
coords,
} = useAddressStore();
const { optionalItems } = useAccordionStore();
const { optionalItems } = useAccordionStore.getState();
const findOrder = (feature: string) => {
if (!feature) return undefined; // feature가 없으면 undefined 반환
const result = optionalItems.find((value) => value.feature === feature);
Expand Down Expand Up @@ -283,7 +285,7 @@ export const getInvitationAction = (): Omit<
};
};

export const useUpdateInvitationStore = (details: InvitationDetail) => {
export const updateInvitationStore = (details: InvitationDetail) => {
const {
setAddress,
setJibunAddress,
Expand Down Expand Up @@ -311,8 +313,9 @@ export const useUpdateInvitationStore = (details: InvitationDetail) => {
useRSVPStore.getState();
const { toggleSubFeature: calendarToggle } =
useCalendarFeatureStore.getState();
const { setOrderItems, setOptionalItems } = useAccordionStore.getState();

if (details) {
try {
updateBrideGroom(0, 'name', details.groomName);
updateBrideGroom(1, 'name', details.brideName);
updateFamily(1, 'father', 'name', details.brideFatherName);
Expand Down Expand Up @@ -580,6 +583,23 @@ export const useUpdateInvitationStore = (details: InvitationDetail) => {
{ key: 'contact', list: contactInfo },
{ key: 'notice', list: noticesData },
];

const newOptionalItems = useAccordionStore
.getState()
.optionalItems.map((item) => {
const feature = features.find((f) => f.key === item.feature);
//feature.list[0].order는 details Props 에서 받아온 값
if (feature && Array.isArray(feature.list) && feature.list.length > 0) {
//해당 props의 순서를 받아와서
const order = feature.list[0].order;
//반환
return order !== undefined ? { ...item, order } : item;
}
return item;
});
setOptionalItems(newOptionalItems);
setOrderItems();
// isActive 동기화
features.forEach(({ key, list }) => {
const isActive =
Array.isArray(list) && list.length > 0 && list[0].isActive;
Expand All @@ -590,5 +610,7 @@ export const useUpdateInvitationStore = (details: InvitationDetail) => {
selectMusic(details.audio);
}
musicToggle('music', !!details.audio); //수정 필요
} catch (err) {
throw new Error(`수정중 청첩장 불러오는 에러 발생 : ${err}`);
}
};
Loading