Skip to content

[김예인] 스프린트 미션 3#36

Open
yein0528 wants to merge 4 commits into
codeit-sprint-fullstack:basicfrom
yein0528:basic-김예인-sprint3

Hidden character warning

The head ref may contain hidden characters: "basic-\uae40\uc608\uc778-sprint3"
Open

[김예인] 스프린트 미션 3#36
yein0528 wants to merge 4 commits into
codeit-sprint-fullstack:basicfrom
yein0528:basic-김예인-sprint3

Conversation

@yein0528

@yein0528 yein0528 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

스프린트 미션 요구사항 체크리스트

v3

공통 (API 구현 및 비동기 처리)

  • Github에 스프린트 미션 PR을 만들어 주세요.
  • 'https://panda-market-api-crud.vercel.app/docs/#/Article' API를 이용하여 아래 함수들을 구현해 주세요.
    • getArticleList() : GET 메서드를 사용해 주세요. (page, pageSize, keyword 쿼리 파라미터 이용)
    • getArticle() : GET 메서드를 사용해 주세요.
    • createArticle() : POST 메서드를 사용해 주세요. (request body에 title, content, image 포함)
    • patchArticle() : PATCH 메서드를 사용해 주세요.
    • deleteArticle() : DELETE 메서드를 사용해 주세요.
  • fetch 혹은 axios 를 이용해 주세요.
  • 응답의 상태 코드가 2XX가 아닐 경우, 에러메시지를 콘솔에 출력해 주세요.
  • .then() 메서드를 이용하여 비동기 처리를 해주세요.
  • .catch() 를 이용하여 오류 처리를 해주세요.
  • 'https://panda-market-api-crud.vercel.app/docs/#/Product' API를 이용하여 아래 함수들을 구현해 주세요.
    • getProductList() : GET 메서드를 사용해 주세요. (page, pageSize, keyword 쿼리 파라미터 이용)
    • getProduct() : GET 메서드를 사용해 주세요.
    • createProduct() : POST 메서드를 사용해 주세요. (request body에 name, description, price, tags, images 포함)
    • patchProduct() : PATCH 메서드를 사용해 주세요.
    • deleteProduct() : DELETE 메서드를 사용해 주세요.
  • async/await 을 이용하여 비동기 처리를 해주세요.
  • try/catch 를 이용하여 오류 처리를 해주세요.
  • 구현한 함수들을 아래와 같이 파일을 분리해 주세요.
    • export를 활용해 주세요.
    • ProductService.js 파일에 Product API 관련 함수들을 작성해 주세요.
    • ArticleService.js 파일에 Article API 관련 함수들을 작성해 주세요.
  • 이외의 코드들은 모두 main.js 파일에 작성하고 import를 활용해 주세요.
  • 각 함수를 실행하는 코드를 작성하고, 제대로 동작하는지 확인해 주세요.

로그인 / 회원가입 페이지 공통 (유효성 검증)

  • "판다마켓" 로고 클릭 시 루트 페이지("/")로 이동합니다.
  • SNS 아이콘들은 클릭 시 각각 구글, 카카오 페이지로 이동합니다.
  • input 요소에 focus in 일 때, 테두리 색상은 #3692FF입니다.
  • input 요소에 focus out 일 때, 테두리는 없습니다.
  • 로그인 및 회원가입 페이지의 이메일, 비밀번호, 비밀번호 확인 input에 필요한 유효성 검증 함수를 만들고 적용해 주세요.
  • 이메일 input에서 focus out 할 때, 값이 없을 경우 input에 빨간색 테두리와 아래에 “이메일을 입력해주세요.” 빨간색 에러 메세지를 보입니다.
  • 이메일 input에서 focus out 할 때, 이메일 형식에 맞지 않는 경우 input에 빨간색 테두리와 아래에 “잘못된 이메일 형식입니다” 빨간색 에러 메세지를 보입니다.
  • 비밀번호 input에서 focus out 할 때, 값이 없을 경우 아래에 “비밀번호를 입력해주세요.” 에러 메세지를 보입니다.
  • 비밀번호 input에서 focus out 할 때, 값이 8자 미만일 경우 아래에 “비밀번호를 8자 이상 입력해주세요.” 에러 메세지를 보입니다.
  • input 에 빈 값이 있거나 에러 메세지가 있으면 ‘로그인’ 버튼은 비활성화 됩니다.
  • input 에 유효한 값을 입력하면 ‘로그인' 버튼이 활성화 됩니다.
  • 활성화된 ‘로그인’ 버튼을 누르면 “/items” 로 이동합니다.

로그인 페이지

  • "회원가입" 버튼 클릭 시 "/signup" 페이지로 이동합니다.
  • 이메일과 비밀번호를 입력하고 로그인 버튼을 누른 후, 다음 조건을 참조하여 로그인 성공 여부를 alert 메시지로 출력합니다.
    • 입력한 이메일이 데이터베이스(USER_DATA)에 없거나 비밀번호가 틀린 경우, '비밀번호가 일치하지 않습니다.' alert 표시
    • 입력한 이메일과 비밀번호가 일치할 경우, “/items”로 이동

회원가입 페이지

  • "로그인" 버튼 클릭 시 "/login" 페이지로 이동합니다.
  • 회원가입을 위해 이메일, 닉네임, 비밀번호, 비밀번호 확인을 입력한 뒤, 회원가입 버튼을 클릭 시 다음 조건에 따라 처리합니다.
    • 입력한 이메일이 이미 데이터베이스(USER_DATA)에 존재하는 경우, '사용 중인 이메일입니다' alert 표시
    • 존재하지 않는 경우 회원가입 성공 처리 후 로그인 페이지(”/login”)로 이동

심화 요구사항

랜딩 페이지 & 공통 메타 태그

  • palette에 있는 color값들을 css 변수로 등록해서 사용합니다.
  • 구글 애널리틱스로 방문자 수 확인하기 할 수 있도록 설정합니다.
  • 페이스북, 카카오톡, 디스코드, 트위터 등 SNS 공유 시 미리보기를 볼 수 있도록 랜딩 페이지 메타 태그를 설정합니다. (OG Tag)
    • 미리보기 제목: “판다마켓”
    • 미리보기 설명: “일상에서 모든 물건을 거래해보세요”
  • 로그인, 회원가입 페이지에 공통으로 사용하는 로직이 있다면, 반복하지 않고 공통된 로직을 모듈로 분리해 사용해 주세요.

반응형 디자인 (랜딩 페이지)

  • 브라우저 viewport 너비를 기준으로 분기되는 반응형 디자인을 적용합니다. (PC: 1200px 이상 / Tablet: 744px1199px / Mobile: 375px743px)
  • Tablet 사이즈로 작아질 때 최소 좌우 여백("판다마켓" 로고 왼쪽 24px, "로그인" 버튼 오른쪽 24px)을 유지하도록 로고와 버튼의 간격이 조절됩니다.
  • Mobile 사이즈로 작아질 때 최소 좌우 여백("판다마켓" 로고 왼쪽 16px, "로그인" 버튼 오른쪽 16px)을 유지하도록 로고와 버튼의 간격이 조절됩니다.
  • PC, Tablet 사이즈의 이미지 크기는 고정값을 사용합니다.
  • Mobile 사이즈의 이미지는 좌우 여백 32px을 제외하고 이미지 영역이 꽉 차게 구현합니다. (가로 비율에 맞춰 세로 유동적 조절)
  • Mobile 사이즈 너비가 커지면, 하단 푸터 영역 요소들과 SNS 아이콘들 영역 사이의 간격이 늘어납니다.

반응형 디자인 및 컴포넌트 기능 (로그인 / 회원가입)

  • Tablet 사이즈에서 내부 디자인은 PC사이즈와 동일합니다.
  • Mobile 사이즈에서 좌우 여백 16px 제외하고 내부 요소들이 너비를 모두 차지합니다.
  • Mobile 사이즈에서 내부 요소들의 너비는 기기의 너비가 커지는 만큼 커지지만 최대 400px을 넘지 않습니다.
  • 오류 메시지 모달을 구현합니다. 모달 내 내용은 alert 메시지와 동일합니다.
  • 비밀번호, 비밀번호 확인 input 오른쪽 눈 모양 아이콘을 클릭하여 비밀번호 표시/숨기기 토글이 가능합니다. (기본 상태: 숨김)

배포 링크

멘토님에게 남길 메시지

아직 코드 작성에 미숙하다보니 놓치는 부분이 많았는데 세세한 부분까지 다 피드백해주셔서 도움이 정말 많이 됐습니다! 이번 미션이 워낙 어려웠어서 코드가 들쑥날쑥한 점 미리 죄송하고 이번에도 잘부탁드립니다!
반응형 디자인이 제대로 구현이 되었는지 자세히 봐주시면 감사하겠습니다!

@ckdqja135 ckdqja135 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예인님 ~ PR 잘 봤습니다!ㅎㅎㅎ

먼저 코드가 들쑥날쑥해서 죄송하다고 하셨는데 전혀 죄송할 일 아니에요. 이번 미션은 정말 분량이 많고, API 두 종류 * CRUD 5개 + 로그인/회원가입 + 반응형 + 모달 + 비번 토글까지 한 미션에 이 정도 내용이 들어 있는 건 입문자에게 결코 만만한 양이 아닙니다. 끝까지 제출하신 것만으로도 충분히 잘하신 거예요.

먼저 칭찬하고 싶은 건 파일 구조 감각입니다. validation.js, modal.js 같은 작은 모듈을 따로 만들고login/signup 양쪽에서 import해서 쓰는 흐름들은 미션 요구사항을 진짜로 이해해서 만든 결과예요.

단순히 분리하라니까 분리한 게 아니라 어떤 코드가 어디에 들어가야 하는지에 대한 감이 잡혀가는 게 보입니다. 그리고 모바일에서 이미지에 calc(100vw - 32px)를 쓰신 부분, .second-info-card에 column-reverse를 쓰신 부분 모두 왜 이렇게 해야 하는지를 이해하지 못하면 못쓰는 코드예요. 디테일을 챙기는 감각이 좋습니다. 👍

그럼에도 불구하고 이번에 가장 중요하게 봐주셨으면 하는 건 두 가지예요.
첫째 signupstyle.css의 미디어 쿼리가 .login-container를 가리키고 있어서 회원가입 페이지에 반응형이 전혀 안 먹고 있는 상황입니다. 직접 요청해주신 반응형이 제대로 구현됐는지의 답이 안타깝게도 랜딩/로그인은 됐는데 회원가입은 안 됐다.입니다. 하지만 이건 한 줄 고치면 해결되는 문제이니 가장 먼저 손봐주세요!
둘째 indexstyle.css의 라인 184~498이 라인 501~826에 거의 통째로 한 번 더 복붙되어 있는 상태예요. 작업 도중 실수로 들어간 것 같은데 절반 정도 분량이 잉여 코드라 아쉬운 포인트였어요 ㅎㅎ

JS 쪽도 같은 맥락이에요. validation.js의 setError가 텍스트만 바꾸고 input에 빨간 테두리는 입히지 않는 점, 그리고 signup.js의 checkPasswordConfirm에 return false가 한 줄 빠져서 빈 값에서도 검증이 통과되는 점 둘 다 화면상으로는 동작하는 것처럼 보이지만 기능의 절반이 빠진 상태라서 꼭 짚어드리고 싶었어요.

마지막으로 주어진 미션만을 보았을 때 제가 캐치했던 출제자의 의도 한 가지만 더 말씀드리자면 Article은 .then()/.catch(), Product는 async/await + try/catch 로 일부러 방식이 다르게 요구된 부분이에요. 지금은 두 서비스 모두 async/await로 통일되어 있는데 두 가지 비동기 처리 방식을 모두 경험해보는 게 이번 미션의 학습 목표라서 Article 쪽만 .then() 체인으로 다시 써보시면 좋겠습니다.
이미 async/await은 능숙하게 쓰시는 게 보이니 두 방식을 직접 비교해보기 딱 좋은 기회가 될 거예요.

전반적으로 코드를 작성할 수 있다는 단계는 충분히 넘어오신 상태이고 지금부터는 작성한 코드를 다시 한 번 점검하는 습관이 큰 도움이 될 거예요. 작업이 끝났을 때 파일을 한 번 위에서 아래로 훑어보고(중복 블록이 있는지), 브라우저 DevTools에서 각 화면 폭(375/744/1200/1920)에서 실제로 어떻게 보이는지만 체크해보시면 이번 미션의 이슈 중 80%는 미리 발견하실 수 있어요.

다음 미션도 화이팅이에요! 어렵다고 느끼시는 부분이 있으면 지금처럼 구체적으로 요청해주세요. 그래야 도움이 더 됩니다. 😃

Comment thread css/signupstyle.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

미디어 쿼리가 엉뚱한 클래스를 가리킵니다. 회원가입 페이지에 반응형이 적용되지 않고 있어요.

signup.html은 컨테이너로 .signup-container를 쓰고 있는데 signupstyle.css의 미디어 쿼리는 이렇게 되어 있어요.

/* signupstyle.css 라인 377~ */
@media (max-width: 1119px) {                                                                                          
  .login-container {  /* ← .signup-container 가 아니라 .login-container! */
    max-width: 640px;                                                                                                 
    padding: 0  
  }                                                                                                                   
}               

@media (max-width: 743px) {
  .login-container {  /* ← 여기도 마찬가지 */
    max-width: 400px;                                                                                                 
    ...
  }                                                                                                                   
}               

signup.html에는 .login-container라는 요소가 존재하지 않습니다. 즉, 회원가입 페이지는 어떤 화면 크기에서도 반응형 스타일이 적용되지 않고 있어요. 모바일에서 회원가입 페이지를 열어보면 PC용 레이아웃 그대로 보일 거예요.

왜 문제인지
login용 CSS를 복사해서 signup용으로 만드는 과정에서 클래스명을 바꿔야 하는 곳을 놓치신 것 같습니다. 이건 미션 요구사항의 "회원가입 모바일 좌우 16px / 최대 400px"이 그대로 구현 누락된 셈이에요. ㅎㅎㅎ

signupstyle.css의 미디어 쿼리 두 곳에서 .login-container → .signup-container로 바꿔주세요.

CSS의 클래스 선택자는 HTML과 1:1로 매칭되어야 동작합니다. CSS는 "이 클래스 이름을 가진 요소가 있으면 적용한다"고만
약속하기 때문에 HTML에 없는 이름이면 조용히 무시되고 에러도 안 나요.
이래서 복사-수정 작업에서 가장 잘 새는 버그 유형이고 브라우저 DevTools의 Elements 패널에서 요소를 클릭했을 때 어떤 CSS 규칙이 매칭됐는가를 직접 확인하는 습관을 들이면 빨리 발견할 수 있습니다. ㅎㅎㅎ

Comment thread css/indexstyle.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

거의 동일한 코드 블록이 두 번 반복됩니다.(라인 184498와 501826)

home-top-image, .info-section, .info-card, .info-text, .second-info-section, .second-info-card, .third-info-section, .third-info-card, .bottom-banner, .banner-inner, .footer, .footer-inner, .footer-links, .sns-links — 이 선택자들의 규칙이 사실상 그대로 한 번 더 정의되어 있어요. @media (min-width: 120rem) 블록도 라인 485와 813에 똑같이 두 번 있습니다.

이 부분이 왜 문제냐면

  • CSS는 나중에 선언된 규칙이 이긴다(cascade)는 특성 때문에, 두 블록의 값이 살짝 달라지는 순간 어느 쪽이 적용되는지 추적이 매우 어려워집니다. 미래에 한쪽만 수정하면 다른 쪽이 남아서 의도와 다른 결과가 나옵니다.
  • 파일 크기가 거의 두 배가 돼서 가독성과 유지보수 비용이 큽니다 (현재 972줄 중 절반 가까이가 잉여 코드).
  • 채점자/리뷰어가 보면 "왜 이렇게 됐지?" 라는 의문이 생겨서 안 좋은 인상이 생깁니다.

따라서 라인 501826 블록 전체와, 두 번째 @media (min-width: 120rem) 블록(라인 813)을 삭제하시면 됩니다. 삭제 후 페이지가 정상으로 보이는지 한 번 확인해주세요.

CSS의 cascade(폭포) 규칙은 "동일한 specificity면 나중에 쓰인 게 이긴다"입니다. 따라서 의도치 않은 중복은 디버깅을 매우
어렵게 만듭니다. 코드를 잘라 붙이다가 실수로 두 번 붙여넣은 것 같은데 이런 사고를 막으려면 작업 후 파일 길이를 한 번 훑고 Ctrl+F로 같은 셀렉터가 여러 번 등장하는지 검색하는 습관이 도움이 됩니다. ㅎㅎㅎ

Comment thread css/indexstyle.css
Comment thread css/indexstyle.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 indexstyle.css에 PC(1200~1919) 구간을 따로 정의한 부분이 없어서 기본값으로 굴러갑니다. 기본 padding이
.info-section { padding: 8.625rem 21.5rem } (좌우 344px씩) 인데 1200~1500px 화면에서는 콘텐츠 공간이 거의 안 남아요. PC 기본값을 1200px 화면에서도 자연스럽게 보이도록 손보면 더 완성도가 높아질거에요 ㅎㅎ

Comment thread js/validation.js

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빨간 테두리가 적용되지 않고 있어용

미션 요구사항은 빈 값/잘못된 형식이면 빨간 테두리 + 에러 메시지인데 현재 validation.js의 setError 함수는 텍스트만
바꾸고 끝납니다.

export function setError(messageEl, message) {                                                                        
  messageEl.textContent = message  // ← 텍스트만 변경
}          

loginstyle.css에는 .input-error { border: 1px solid var(--error-red) !important; } 클래스가 정의되어 있는데
이걸 input에 추가하는 코드가 어디에도 없어요. 즉 클래스는 만들어두고 사용은 안 한 상태입니다.

그래서 아래와 같이 수정해보세용 ㅎㅎ

function checkEmail() {
   const email = emailInput.value;
                                                                                                                       
   clearError(emailInput, emailError);
                                                                                                                       
   if (!email) { 
     setError(emailInput, emailError, '이메일을 입력해주세요.');
     return false;                                              
   }                                                                                                                   
    
   if (!validateEmail(email)) {                                                                                        
     setError(emailInput, emailError, '잘못된 이메일 형식입니다.');
     return false;                                                 
   }              
    
   return true;                                                                                                        
 }             
                                                                                                                       
 function checkPassword() {
   const password = passwordInput.value;
                                        
   clearError(passwordInput, passwordError);
                                            
   if (!password) {
     setError(passwordInput, passwordError, '비밀번호를 입력해주세요.');
     return false;                                                      
   }                                                                                                                   
    
   if (!validatePassword(password)) {                                                                                  
     setError(passwordInput, passwordError, '비밀번호를 8자 이상 입력해주세요.');
     return false;
   }                                                                                                                   
  
   return true;                                                                                                        
 }

Comment thread js/ProductService.js
Comment thread js/main.js
const product = await getProduct(newProduct.id);
console.log('아까 생성한 상품:', product);

const patchproduct = await patchProduct(newProduct.id, '수정한과자이름');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const patchproduct = await patchProduct(newProduct.id, '수정한과자이름');

patchProduct(productId, name, description, price, tags, images) 시그니처인데 name만 전달하면 나머지는 undefined로 채워지고 그게 그대로 body에 들어갑니다. PATCH 의도는 바꿀 필드만 전달이라서 이 경우 객체에서 undefined 값을 빼주거나 body를 객체로 받도록 함수 시그니처를 바꿔주는 게 더 안전해요. (예: patchProduct(id, partialData) 형태)

아래와 같이 수정해주세요~

 const patchproduct = await patchProduct(newProduct.id, {                                                                                                                                                                                                                                              
    name: '수정한과자이름'
  });                                                                                                                                                                                                                                                                                                   
  console.log('수정한 상품: ', patchproduct);

요렇게 객체 기반으로 바꾸는 방식이 가장 깔끔해요~ ㅎㅎ

다른 곳도 이런 포맷으로 바꾸면 더욱 좋아용

Comment thread js/ArticleService.js
keyword = '',
orderBy = 'recent') {
try {
const query =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿼리스트링은 URLSearchParams 사용하는 것을 권장합니다.

아래와 같이요~ ㅎㅎㅎ

async function getArticleList(
    page = 1,                                                                                                                                                                                                                                                                                           
    pageSize = 10,
    keyword = '',                                                                                                                                                                                                                                                                                       
    orderBy = 'recent') {
    try {                
      const query = new URLSearchParams({
        page,                            
        pageSize,                                                                                                                                                                                                                                                                                       
        keyword, 
        orderBy,                                                                                                                                                                                                                                                                                        
      });         
         
      const response = await fetch(`${BASE_URL}/articles?${query}`);

이렇게 하면 예를 들어 keyword에 한글이나 특수문자가 들어왔을 때 자동으로 변환됩니다.
// keyword: '안녕 & 반가워'
// 기존 방식: ?keyword=안녕 & 반가워 ← URL 깨짐 (공백, & 가 그대로 들어감)
// URLSearchParams: ?keyword=%EC%95%88%EB%85%95+%26+%EB%B0%98%EA%B0%80%EC%9B%8C

서버가 정확히 "안녕 & 반가워" 로 디코딩해서 받습니다. keyword에 검색어 &page=1처럼 악의적인 문자열이 들어와도 다른 파라미터를 덮어쓰지 못해요. 인코딩 + 보안 + 가독성 세 가지가 한 번에 해결 되는 것이지용~

확인하실 때는 콘솔에 console.log(${BASE_URL}/articles?${query}) 한 줄 찍어보시면 실제로 어떤 URL이 만들어지는지 보입니다만, 지금 단계에서는 '이런 도구가 있다' 정도만 인지하고 다음에 시도해보셔도 충분합니다.

Comment thread css/indexstyle.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexstyle.css 라인 82의 width: 9.5625rem !important; — !important는 cascade를 강제로 깨버려서 나중에 반응형 등으로 로고 크기를 바꾸려고 할 때 다른 규칙이 안 먹히게 만들어요. 우선순위가 필요하면 셀렉터의 specificity(예: .gnb .logoimg)로 해결하시는 편이 안전합니다.

Comment thread css/loginstyle.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexstyle.css는 rem을 주로 쓰지만 loginstyle.css와 signupstyle.css는 px를 주로 씁니다.
한 프로젝트 안에서는 한 단위로 통일하는 게 가독성과 유지보수에 좋아요. (특히 사용자 글자 크기 설정을 고려하면 rem이 더 접근성 친화적입니다.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants