-
Notifications
You must be signed in to change notification settings - Fork 21
[황승찬] Sprint10 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Irelander
merged 11 commits into
codeit-sprint-fullstack:next-황승찬
from
HwSoonDev:next-황승찬-sprint10
Oct 29, 2025
The head ref may contain hidden characters: "next-\uD669\uC2B9\uCC2C-sprint10"
Merged
[황승찬] Sprint10 #148
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
ce38f39
백엔드 폴더 구조 수정
135aa5c
prettier 수정 및 전체 적용
b218b76
게시글, 상품 컴포넌트 수정
e1fdac1
백엔드 auth 라우터 추가, 전체 폴더 리펙토링
3b19fdd
프론트 import 리펙토링
1fcb143
회원가입/로그인 기능 구현 (프론트/백)
1db7cd0
회원가입/로그인 기능 구현 (프론트/백)
2a10e00
상품 이미지 업로드 및 상품페이지 표시 구현 (프론트/백
9812f0e
임시 좋아요 기능
7ca7e51
인증/인가(로그인한 유저만) -> 상품 등록
a1cc699
댓글 작성 인가 기능, 작성자 표시
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,31 @@ | ||
| name: PR 자동 라벨링 및 담당자 할당 | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, reopened] | ||
| pull_request: | ||
| types: [opened, reopened] | ||
|
|
||
| jobs: | ||
| add-labels-assign: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v2 | ||
| add-labels-assign: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v2 | ||
|
|
||
| - name: Add labels and assign PR creator | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PR_NUMBER=${{ github.event.pull_request.number }} | ||
| PR_CREATOR=${{ github.event.pull_request.user.login }} | ||
| - name: Add labels and assign PR creator | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| PR_NUMBER=${{ github.event.pull_request.number }} | ||
| PR_CREATOR=${{ github.event.pull_request.user.login }} | ||
|
|
||
| # Add labels | ||
| curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ | ||
| -H "Accept: application/vnd.github.v3+json" \ | ||
| https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/labels \ | ||
| -d '{"labels":["매운맛🔥", "진행 중 🏃"]}' | ||
| # Add labels | ||
| curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ | ||
| -H "Accept: application/vnd.github.v3+json" \ | ||
| https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/labels \ | ||
| -d '{"labels":["매운맛🔥", "진행 중 🏃"]}' | ||
|
|
||
| # Assign PR creator | ||
| curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ | ||
| -H "Accept: application/vnd.github.v3+json" \ | ||
| https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/assignees \ | ||
| -d "{\"assignees\":[\"$PR_CREATOR\"]}" | ||
| # Assign PR creator | ||
| curl -X POST -H "Authorization: token $GITHUB_TOKEN" \ | ||
| -H "Accept: application/vnd.github.v3+json" \ | ||
| https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUMBER/assignees \ | ||
| -d "{\"assignees\":[\"$PR_CREATOR\"]}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| { | ||
| "printWidth": 100, | ||
| "tabWidth": 4, | ||
| "semi": true, | ||
| "singleQuote": true, | ||
| "trailingComma": "es5" | ||
| "printWidth": 100, | ||
| "tabWidth": 2, | ||
| "semi": true, | ||
| "singleQuote": true, | ||
| "trailingComma": "es5" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| PORT=4000 | ||
|
|
||
| DATABASE_URL="postgresql://postgres:example3@localhost:5432/panda_db?schema=public" | ||
|
|
||
| JWT_SECRET = "myTestSecretKey122333444" | ||
| JWT_ACCESS_EXPIRES_IN = "1h" | ||
| JWT_REFRESH_EXPIRES_IN = "2w" | ||
|
|
||
| GOOGLE_CLIENT_ID = "exampleID" | ||
| GOOGLE_CLIENT_SECRET = "exampleSecret" |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import prisma from '../../config/db.js'; | ||
| import authRepo from '../auth/auth.repository.js'; | ||
|
|
||
| export async function getArticleList(req, res) { | ||
| const { page = 1, pageSize = 10, orderBy = 'recent', keyword = '' } = req.query; | ||
|
|
||
| const limit = parseInt(pageSize); | ||
| const offset = (parseInt(page) - 1) * limit; | ||
|
|
||
| const articles = await prisma.article.findMany({ | ||
| where: { | ||
| // 검색 쿼리 | ||
| OR: [{ title: { contains: keyword } }, { content: { contains: keyword } }], | ||
| }, | ||
| orderBy: orderBy == 'recent' ? { createdAt: 'desc' } : { favoriteCount: 'desc' }, | ||
| skip: parseInt(offset), | ||
| take: parseInt(limit), | ||
| }); | ||
| res.json(articles); | ||
| } | ||
|
|
||
| export async function getArticle(req, res) { | ||
| const { id } = req.params; | ||
| const article = await prisma.article.findUnique({ | ||
| where: { id }, | ||
| }); | ||
| res.json(article); | ||
| } | ||
|
|
||
| //새로 등록 할 때만 유저 정보가 필요합니다. | ||
| export async function createArticle(req, res) { | ||
| const userId = req.auth?.userId; | ||
| const user = await authRepo.findById(userId); | ||
|
|
||
| const article = await prisma.article.create({ | ||
| data: { | ||
| ...req.body, | ||
| userName: user.name, | ||
| user: { connect: { id: user.id } }, | ||
| }, | ||
| }); | ||
|
|
||
| res.status(201).send(article); | ||
| } | ||
|
|
||
| export async function updateArticle(req, res) { | ||
| const { id } = req.params; | ||
| const article = await prisma.article.update({ | ||
| where: { id }, | ||
| data: req.body, | ||
| }); | ||
|
|
||
| res.json(article); | ||
| } | ||
|
|
||
| export async function deleteArticle(req, res) { | ||
| const { id } = req.params; | ||
| await prisma.article.delete({ | ||
| where: { id }, | ||
| }); | ||
| res.sendStatus(204); | ||
| } | ||
|
|
||
| //소유권 검사 미들웨어 | ||
| export async function checkOwner(req, res, next) { | ||
| try { | ||
| const { id } = req.params; | ||
| const userId = req.auth?.userId; | ||
| const product = await prisma.article.findUnique({ | ||
| where: { id }, | ||
| }); | ||
| if (product.userId !== userId) { | ||
| const error = new Error('user is not owner'); | ||
| error.code = 403; | ||
| throw error; | ||
| } | ||
| } catch (err) { | ||
| next(err); | ||
| } finally { | ||
| next(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { Router } from 'express'; | ||
| import { asyncHandeler } from '../../utils/errorHandler.js'; | ||
| import { verifyAccessToken } from '../../middlewares/authGuard.js'; | ||
| import { | ||
| getArticleList, | ||
| getArticle, | ||
| createArticle, | ||
| updateArticle, | ||
| deleteArticle, | ||
| checkOwner, | ||
| } from './articles.controller.js'; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| /* === 게시글 api === */ | ||
| router.get('', asyncHandeler(getArticleList)); | ||
| router.get('/:id', asyncHandeler(getArticle)); | ||
|
|
||
| //로그인한 유저(verifyAccessToken), 소유권이 있는 유저(checkOwner) | ||
| router.post('', verifyAccessToken, asyncHandeler(createArticle)); | ||
| router.patch('/:id', verifyAccessToken, checkOwner, asyncHandeler(updateArticle)); | ||
| router.delete('/:id', verifyAccessToken, checkOwner, asyncHandeler(deleteArticle)); | ||
| router.post('/:id/checkOwner', verifyAccessToken, checkOwner, async (req, res) => { | ||
| res.status(200).json({ owner: true }); | ||
| }); | ||
|
|
||
| export default router; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| import router from './articles.routes.js'; | ||
| export default router; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import { createToken, refreshAccessToken, isValidEmail, isValidToken } from './utils/token.js'; | ||
| import { createUser, getUser, getUserById, updateUser } from './auth.service.js'; | ||
|
|
||
| const refreshTokenCookieOptions = { | ||
| path: '/auth/refresh', //쿠키 경로 고정. | ||
| httpOnly: true, //JS 접근 불가 | ||
| secure: false, //로컬 실험용. -> 배포 true | ||
| sameSite: 'Lax', //로컬 실험용. -> 배포 'None' | ||
| }; | ||
|
|
||
| //signup - 회원가입 email/pw/name -> 새로운 유저 생성. | ||
| export async function signup(req, res, next) { | ||
| try { | ||
| const { name, email, password } = req.body; | ||
| if (!name || !email || !password) { | ||
| const error = new Error('name, email, password 가 모두 필요합니다.'); | ||
| error.code = 422; | ||
| throw error; | ||
| } | ||
|
|
||
| if (!isValidEmail(email)) { | ||
| const error = new Error('email 형식이 올바르지 않습니다.'); | ||
| error.code = 422; | ||
| throw error; | ||
| } | ||
|
|
||
| //createUesr()는 user를 파라미터로 받으므로 {}로 넘겨준다. | ||
| const result = await createUser({ name, email, password }); | ||
| return res.status(201).json(result); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| //login - email/pw를 이용해 엑세스 토큰 생성, 리프레시 토큰 세션/쿠키 포함. | ||
| export async function login(req, res, next) { | ||
| const { email, password } = req.body; | ||
| try { | ||
| if (!email || !password) { | ||
| const error = new Error('email, password 가 모두 필요합니다.'); | ||
| error.code = 422; | ||
| throw error; | ||
| } | ||
|
|
||
| if (!isValidEmail(email)) { | ||
| const error = new Error('email 형식이 올바르지 않습니다.'); | ||
| error.code = 422; | ||
| throw error; | ||
| } | ||
|
|
||
| const user = await getUser(email, password); //실패 시 함수 안에서 에러 throw | ||
| const accessToken = createToken(user); | ||
| const refreshToken = createToken(user, 'refresh'); | ||
|
|
||
| await updateUser(user.id, { refreshToken }); | ||
| res.cookie('refreshToken', refreshToken, refreshTokenCookieOptions); | ||
| res.status(200).json({ ...user, accessToken }); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| //logout - 리프레쉬 토큰을 쿠키에서 지웁니다. (액세스 토큰 지우는 건 프론트에서 처리) | ||
| export async function logout(req, res, next) { | ||
| try { | ||
| res.clearCookie('refreshToken', refreshTokenCookieOptions); | ||
| res.status(200).json({ ok: true }); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| //refresh - 리프레쉬 토큰이 있을 경우, 액세스/리프레쉬 토큰 재발급 | ||
| export async function refresh(req, res, next) { | ||
| try { | ||
| const refreshToken = req.cookies.refreshToken; | ||
| const userId = req.auth?.userId || req.user?.id; | ||
| //auth에 있으면 기존 미들웨어, | ||
| //없으면 passport의 jwtStrategy 미들웨어로 보고 user에서 찾음. | ||
| //console.log("userId: " + userId); | ||
|
|
||
| //jwt 검증으로 저장된 userId라서 id는 굳이 검사과정이 필요 x | ||
|
|
||
| const newAccessToken = await refreshAccessToken(userId, refreshToken); | ||
| const newRefreshToken = createToken({ id: userId }, 'refresh'); //액세스 토큰 갱신과 함께 리프레쉬 토큰도 갱신. 무한로그인 유지 | ||
| await updateUser(userId, { refreshToken: newRefreshToken }); | ||
| res.cookie('refreshToken', newRefreshToken, refreshTokenCookieOptions); | ||
|
|
||
| return res.status(200).json({ | ||
| ok: true, | ||
| accessToken: newAccessToken, | ||
| }); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| //accessToken이 있을 경우 refresh 토큰 발급 | ||
| export async function setRefreshToken(req, res, next) { | ||
| try { | ||
| const userId = req.auth?.userId || req.user?.id; //인증 미들에어를 통해 req로 넘어오는 정보. | ||
| const user = await getUserById(userId); //실패 시 함수 안에서 에러 throw <- userServide.js 함수 | ||
| console.log('userName: ' + user.name); | ||
| const newRefreshToken = createToken(user, 'refresh'); | ||
| console.log('refreshToken: ' + newRefreshToken); | ||
| await updateUser(userId, { refreshToken: newRefreshToken }); | ||
| res.cookie('refreshToken', newRefreshToken, refreshTokenCookieOptions); | ||
|
|
||
| return res.status(200).json({ ok: true }); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| export async function check(req, res, next) { | ||
| try { | ||
| const authHeader = req.headers.authorization; | ||
| if (!authHeader && !authHeader.startsWith('Bearer ')) { | ||
| const error = new Error('token 양식 오류.'); | ||
| error.code = 401; | ||
| throw error; | ||
| } | ||
|
Comment on lines
+118
to
+122
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. authHeader가 falsy한 값이라하면 이미 undefined/null이기때문에 .startsWith가 호출된다면 에러가 발생할꺼 같아요 ㅎㅎ AND 연산이 아닌 OR 연산으로 처리해야 될것 같네요! |
||
| const accessToken = authHeader.split(' ')[1]; | ||
| if (!isValidToken(accessToken)) { | ||
| const error = new Error('UnauthorizedError'); | ||
| error.code = 403; | ||
| throw error; | ||
| } | ||
| return res.status(200).json({ authenticated: true }); | ||
| } catch (error) { | ||
| return res.status(403).json({ authenticated: false }); | ||
| } | ||
| } | ||
|
|
||
| export async function oauthLogin(req, res, next) { | ||
| try { | ||
| const { id } = req.user; | ||
| const user = await getUserById(id); //실패 시 함수 안에서 에러 throw | ||
| //console.log("userName: " + user.name); | ||
| const accessToken = createToken(user); | ||
| const refreshToken = createToken(user, 'refresh'); | ||
|
|
||
| //성공 시 리프레시 토큰은 쿠키에 저장. | ||
| //(백에 저장되는 거라 자동 리프레쉬가 안되네요.) | ||
| res.cookie('refreshToken', refreshToken, refreshTokenCookieOptions); | ||
|
|
||
| //액세스 토큰은 프론트 리디렉트 라우터에 쿼리로 전송. (이게 가장 현실적인 방법인 듯 합니다.) | ||
| //프론트에서 받으면 바로 메인페이지로 페이지를 날리므로 일단 안전합니다. | ||
| res.redirect(`http://localhost:3000/oauth?accessToken=${accessToken}`); | ||
|
|
||
| // 액세스 토큰을 리턴 하는 방식이 아니라 프론트 원래 페이지로 리다이렉트를 해줘야 해서 res.json()은 없음. | ||
| } catch (err) { | ||
| next(err); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클라이언트에서 넘어오는 모든 body 데이터를 스프레드 연산자로 사용하면 위험할수 있어요 ㅎㅎ
필요한 값들만 사용될수 있도록 처리하는게 좋습니다 !