diff --git a/.gitignore b/.gitignore index f27ba0d8..35eed61a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ dist-ssr *.sw? # mission_template_codes 공부용 -mission_template_codes/ \ No newline at end of file +mission_template_codes/* \ No newline at end of file diff --git a/README.md b/README.md index 7000651a..1ea62873 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ | 2 | 2025-03-05 | [#44](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/44) | 회원가입 및 로그인 페이지의 HTML, CSS 구현 | | 3 | 2025-03-07 | [#60](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/60) | 반응형 디자인 구현(desktop-first, 1920px 이상 큰 모니터 기준), breakpoint: 1919px, 1199px, 767px | | 4 | 2025-03-18 | [#101](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/101) | JS기능 추가(DOM 요소 조작 및 이벤트 리스너), 회원가입, 로그인 폼 유효성 검사 | -| 5 | 2025-05-05 | [#](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/) | React, SCSS+CSS modules로 마이그레이션, items 페이지 구현(fetch data, 검색어, 정렬, pagination, 반응형 구현) | -| 6 | 2025-05-0 | [#](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/) | 상품 등록 페이지 구현 | +| 5 | 2025-05-05 | [#](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/181) | React, SCSS+CSS modules로 마이그레이션, items 페이지 구현(fetch data, 검색어, 정렬, pagination, 반응형 구현) | +| 6 | 2025-05-11 | [#](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/) | 상품 등록 페이지 구현 | --- @@ -83,3 +83,23 @@ ``` 마지막에 추가 할 예정 ``` + +## 에러 처리 전략 +> 모든 에러는 사용자에게 UX 혼란을 최소화하기 위한 피드백(UI/토스트 등)을 포함하여 처리됩니다. + +### 1. 라우팅 오류 +- 잘못된 경로 접근 시 → `404 페이지` → 랜딩 페이지로 이동 버튼 + +### 2. 전역 에러 (App 깨짐) +- 앱 전체 서버 에러 → `500 페이지` → 다시 시도 버튼 + +### 3. API 응답 에러 (safeFetch 내부 → 토스트 처리 ) +| 상태 코드 | 처리 방식 | +|-----------|-----------| +| `401` | 인증 필요 안내 토스트 | +| `403` | 접근 권한 없음 안내 토스트 | +| `404` | 없는 리소스 조회 시 토스트 | +| `500~599` | 서버 응답 오류 토스트 노출 | + +### 4. 특정 컴포넌트 렌더 실패 (예: 이미지 리스트 하나가 깨짐) +- 해당 컴포넌트 수준에서 fallback UI 처리 예정 diff --git a/eslint.config.cjss b/eslint.config.cjss deleted file mode 100644 index a90aecbc..00000000 --- a/eslint.config.cjss +++ /dev/null @@ -1,97 +0,0 @@ -const js = require('@eslint/js'); -const globals = require('globals'); -const reactHooks = require('eslint-plugin-react-hooks'); -const reactRefresh = require('eslint-plugin-react-refresh'); -const pluginReact = require('eslint-plugin-react'); -const tseslint = require('typescript-eslint'); -const prettierPlugin = require('eslint-plugin-prettier'); -const importPlugin = require('eslint-plugin-import'); - -module.exports = [ - { ignores: ['dist', 'eslint.config.cjs'] }, - ...tseslint.configs.recommended, - { - files: ['**/*.{js,jsx,ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - prettier: prettierPlugin, - react: pluginReact, - import: importPlugin, - }, - rules: { - ...js.configs.recommended.rules, - ...reactHooks.configs.recommended.rules, - ...pluginReact.configs.flat.recommended.rules, - 'react/prop-types': 'off', - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - 'prettier/prettier': 'error', - 'react/react-in-jsx-scope': 'off', - - 'import/order': [ - 'warn', - { - groups: [ - 'builtin', - 'external', - 'internal', - ['parent', 'sibling', 'index'], - 'object', - 'type', - ], - pathGroups: [ - { - pattern: '@/**', - group: 'internal', - position: 'after', - }, - { - pattern: '**/*.module.scss', - group: 'index', - position: 'after', - }, - { - pattern: '**/*.scss', - group: 'index', - position: 'after', - }, - { - pattern: '**/*.css', - group: 'index', - position: 'after', - }, - ], - pathGroupsExcludedImportTypes: ['builtin'], - 'newlines-between': 'never', - alphabetize: { - order: 'asc', - caseInsensitive: true, - }, - }, - ], - }, - - settings: { - react: { version: 'detect' }, - 'import/resolver': { - alias: { - map: [['@', './src']], - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - }, - }, -]; diff --git a/eslint.config.js b/eslint.config.js index 98b12d9f..a590e30a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -86,17 +86,12 @@ export default [ position: 'after', }, { - pattern: '**/*.css', - group: 'index', - position: 'after', - }, - { - pattern: '**/*.scss', - group: 'index', + pattern: '@/**/*.module.scss', + group: 'internal', position: 'after', }, { - pattern: '**/*.module.scss', + pattern: './*.module.scss', group: 'index', position: 'after', }, diff --git a/mission_template_codes/mission2-template-code/.vscode/settings.json b/mission_template_codes/mission2-template-code/.vscode/settings.json deleted file mode 100644 index 6b665aaa..00000000 --- a/mission_template_codes/mission2-template-code/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} diff --git a/mission_template_codes/mission4-template-code/faq.html b/mission_template_codes/mission4-template-code/faq.html deleted file mode 100644 index e9ac407d..00000000 --- a/mission_template_codes/mission4-template-code/faq.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - 판다마켓 - FAQ - - - - - - -

임시 FAQ 페이지

- - diff --git a/mission_template_codes/mission4-template-code/images/home/bottom-banner-image.png b/mission_template_codes/mission4-template-code/images/home/bottom-banner-image.png deleted file mode 100644 index 4a5f85b2..00000000 Binary files a/mission_template_codes/mission4-template-code/images/home/bottom-banner-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/home/feature1-image.png b/mission_template_codes/mission4-template-code/images/home/feature1-image.png deleted file mode 100644 index 4684b9a7..00000000 Binary files a/mission_template_codes/mission4-template-code/images/home/feature1-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/home/feature2-image.png b/mission_template_codes/mission4-template-code/images/home/feature2-image.png deleted file mode 100644 index 31e20b97..00000000 Binary files a/mission_template_codes/mission4-template-code/images/home/feature2-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/home/feature3-image.png b/mission_template_codes/mission4-template-code/images/home/feature3-image.png deleted file mode 100644 index 5b8084a7..00000000 Binary files a/mission_template_codes/mission4-template-code/images/home/feature3-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/home/hero-image.png b/mission_template_codes/mission4-template-code/images/home/hero-image.png deleted file mode 100644 index d28fb652..00000000 Binary files a/mission_template_codes/mission4-template-code/images/home/hero-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/icons/eye-invisible.svg b/mission_template_codes/mission4-template-code/images/icons/eye-invisible.svg deleted file mode 100644 index 92252b05..00000000 --- a/mission_template_codes/mission4-template-code/images/icons/eye-invisible.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/mission_template_codes/mission4-template-code/images/icons/eye-visible.svg b/mission_template_codes/mission4-template-code/images/icons/eye-visible.svg deleted file mode 100644 index 35a75305..00000000 --- a/mission_template_codes/mission4-template-code/images/icons/eye-visible.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/mission_template_codes/mission4-template-code/images/logo/favicon.ico b/mission_template_codes/mission4-template-code/images/logo/favicon.ico deleted file mode 100644 index 9fecc692..00000000 Binary files a/mission_template_codes/mission4-template-code/images/logo/favicon.ico and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/logo/logo.svg b/mission_template_codes/mission4-template-code/images/logo/logo.svg deleted file mode 100644 index d497acbf..00000000 --- a/mission_template_codes/mission4-template-code/images/logo/logo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/mission_template_codes/mission4-template-code/images/logo/og-image.png b/mission_template_codes/mission4-template-code/images/logo/og-image.png deleted file mode 100644 index e7d7b5bf..00000000 Binary files a/mission_template_codes/mission4-template-code/images/logo/og-image.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/social/facebook-logo.svg b/mission_template_codes/mission4-template-code/images/social/facebook-logo.svg deleted file mode 100644 index 8491c2f8..00000000 --- a/mission_template_codes/mission4-template-code/images/social/facebook-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/mission_template_codes/mission4-template-code/images/social/google-logo.png b/mission_template_codes/mission4-template-code/images/social/google-logo.png deleted file mode 100644 index 199f3d62..00000000 Binary files a/mission_template_codes/mission4-template-code/images/social/google-logo.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/social/instagram-logo.svg b/mission_template_codes/mission4-template-code/images/social/instagram-logo.svg deleted file mode 100644 index c83306f8..00000000 --- a/mission_template_codes/mission4-template-code/images/social/instagram-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/mission_template_codes/mission4-template-code/images/social/kakao-logo.png b/mission_template_codes/mission4-template-code/images/social/kakao-logo.png deleted file mode 100644 index bfadc1d3..00000000 Binary files a/mission_template_codes/mission4-template-code/images/social/kakao-logo.png and /dev/null differ diff --git a/mission_template_codes/mission4-template-code/images/social/twitter-logo.svg b/mission_template_codes/mission4-template-code/images/social/twitter-logo.svg deleted file mode 100644 index 14a6069a..00000000 --- a/mission_template_codes/mission4-template-code/images/social/twitter-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/mission_template_codes/mission4-template-code/images/social/youtube-logo.svg b/mission_template_codes/mission4-template-code/images/social/youtube-logo.svg deleted file mode 100644 index 5fcc0ff3..00000000 --- a/mission_template_codes/mission4-template-code/images/social/youtube-logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/mission_template_codes/mission4-template-code/index.html b/mission_template_codes/mission4-template-code/index.html deleted file mode 100644 index fd4000c2..00000000 --- a/mission_template_codes/mission4-template-code/index.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - 판다마켓 - - - - - - - -
- 판다마켓 로고 - 로그인 -
- -
- - -
-
- 인기 상품 -
-

Hot item

-

- 인기 상품을
확인해 - 보세요 -

-

- 가장 HOT한 중고거래 물품을
판다마켓에서 확인해 보세요 -

-
-
-
- 검색 기능 -
-

Search

-

- 구매를 원하는
상품을 - 검색하세요 -

-

- 구매하고 싶은 물품은 검색해서 -
쉽게 찾아보세요 -

-
-
-
- 판매 상품 등록 -
-

Register

-

- 판매를 원하는
상품을 - 등록하세요 -

-

- 어떤 물건이든 판매하고 싶은 상품을 -
쉽게 등록하세요 -

-
-
-
- - -
- - - - diff --git a/mission_template_codes/mission4-template-code/items.html b/mission_template_codes/mission4-template-code/items.html deleted file mode 100644 index 7c4f1256..00000000 --- a/mission_template_codes/mission4-template-code/items.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - 판다마켓 - 상품 리스트 - - - - - - -

임시 상품 리스트 페이지

- - diff --git a/mission_template_codes/mission4-template-code/login.html b/mission_template_codes/mission4-template-code/login.html deleted file mode 100644 index 5c35fda1..00000000 --- a/mission_template_codes/mission4-template-code/login.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - 판다마켓 - 로그인 - - - - - - - - - -
- 판다마켓 로고 - -
-
- - - 이메일을 입력해 주세요 - 잘못된 이메일 형식입니다 -
- -
- -
- - -
- 비밀번호를 입력해 주세요 - 비밀번호를 8자 이상 입력해 주세요 -
- - -
- -
-

간편 로그인하기

- -
- -
- 판다마켓이 처음이신가요? 회원가입 -
-
- - - - diff --git a/mission_template_codes/mission4-template-code/privacy.html b/mission_template_codes/mission4-template-code/privacy.html deleted file mode 100644 index aba5f8ee..00000000 --- a/mission_template_codes/mission4-template-code/privacy.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - 판다마켓 - 이용약관 - - - - - - -

임시 이용약관 페이지

- - diff --git a/mission_template_codes/mission4-template-code/scripts/auth.js b/mission_template_codes/mission4-template-code/scripts/auth.js deleted file mode 100644 index cdb910ad..00000000 --- a/mission_template_codes/mission4-template-code/scripts/auth.js +++ /dev/null @@ -1,229 +0,0 @@ -// 선택사항: `DOMContentLoaded` 이벤트 리스너를 사용해 DOM 요소들이 완전히 로드된 후에 이벤트 리스너를 등록하면, 스크립트 태그의 위치와 상관 없이 DOM 요소를 안전하게 참조할 수 있어요. -// 현재 HTML 구조에서는 자바스크립트 파일이 문서의 마지막에 위치해 있기 때문에 DOMContentLoaded 없이 바로 이벤트 리스너들을 추가해도 문제 없어요. -// 스크립트의 위치를 문서 상단으로 이동하거나, 동적으로 스크립트를 로드하는 경우에는 DOMContentLoaded 이벤트 리스너 내부에서 이벤트 리스너들을 등록하는 것이 안전해요. - -document.addEventListener('DOMContentLoaded', () => { - // 각 필드의 유효성 검사 상태를 저장하는 전역 변수 - let isEmailValid = false; - let isNicknameValid = false; - let isPasswordValid = false; - let isPasswordConfirmationValid = false; - - // ID를 통해 타겟 DOM 요소에 접근 - const loginForm = document.getElementById('loginForm'); - const signupForm = document.getElementById('signupForm'); - const emailInput = document.getElementById('email'); - const nicknameInput = document.getElementById('nickname'); - const passwordInput = document.getElementById('password'); - const passwordConfirmationInput = document.getElementById( - 'passwordConfirmation', - ); - const submitButton = document.querySelector( - '.auth-container form button[type="submit"]', - ); - - // 오류 메세지 노출 함수 (오류 메시지 을 visible하게 만들고 입력 필드에 빨간색 테두리를 추가) - // - 코드 중복을 줄이기 위해 반복되는 코드를 함수화해 주었어요. (DRY 원칙, "Don't Repeat Yourself") - // - 오류 메시지 요소 직접 접근 대신, 오류 메시지 ID를 함수에 전달 - function showError(input, errorId) { - const errorElement = document.getElementById(errorId); - errorElement.style.display = 'block'; - input.style.border = '1px solid #f74747'; - } - - // 상태 초기화 함수 (오류 메시지를 숨기고 입력 필드의 테두리를 기본 상태로 리셋) - function hideError(input, errorId) { - const errorElement = document.getElementById(errorId); - errorElement.style.display = 'none'; - input.style.border = 'none'; - } - - // 이메일 유효성 검증 util function - // - 정규표현식(Regular Expression, Regex)을 통해 입력된 값이 기본적인 이메일 형식을 따르고 있는지 확인 후 boolean을 리턴합니다. - // - 이메일 형식을 검증하는 다양한 정규식이 존재하는데 너무 엄격하지도, 너무 느슨하지도 않은 실용적인 버전을 사용하는 게 좋아요. - // - 예시에 사용된 정규식은 보편적으로 사용되는 이메일 주소 형식에 대해서는 높은 검증 성공률을 보이지만, 특수한 도메인의 이메일을 포착하는 데에는 한계가 있을 수 있기 때문에 완벽한 솔루션은 아니에요. - function validateEmailString(email) { - const emailRegex = /^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/; - return emailRegex.test(email); - } - - // 이메일 필드의 유효성 검사 (입력 여부 및 형식) - function checkEmailValidity() { - const emailValue = emailInput.value.trim(); - - // 오류 메세지 및 입력 필드의 상태를 먼저 초기화 - // - 사용자가 입력한 값이 유효성 검사를 통과하지 못해 오류 메시지가 한 번 표시된 이후 입력값을 수정하여 필드가 유효한 상태가 되었을 때 오류 메시지를 다시 숨김 처리하기 위한 용도 - // - 두 가지 오류 메세지가 동시에 노출되지 않도록 하기 위한 용도 - isEmailValid = false; - hideError(emailInput, 'emailEmptyError'); - hideError(emailInput, 'emailInvalidError'); - - if (!emailValue) { - showError(emailInput, 'emailEmptyError'); - } else if (!validateEmailString(emailValue)) { - showError(emailInput, 'emailInvalidError'); - } else { - isEmailValid = true; - hideError(emailInput, 'emailEmptyError'); - hideError(emailInput, 'emailInvalidError'); - } - // 어느 순서로 input을 입력할지 모르니 매번 submit button 활성화해야 하는지 체크하는 것이 안전해요. (추후에 React state을 사용하면 해결되는 문제!) - updateSubmitButtonState(); - } - - // 닉네임 필드의 유효성 검사 - function checkNicknameValidity() { - const nicknameValue = nicknameInput.value.trim(); - isNicknameValid = false; - hideError(nicknameInput, 'nicknameEmptyError'); - - if (!nicknameValue) { - showError(nicknameInput, 'nicknameEmptyError'); - } else { - isNicknameValid = true; - hideError(emailInput, 'nicknameEmptyError'); - } - updateSubmitButtonState(); - } - - // 비밀번호 필드의 유효성 검사 - function checkPasswordValidity() { - const passwordValue = passwordInput.value.trim(); - isPasswordValid = false; - - hideError(passwordInput, 'passwordEmptyError'); - hideError(passwordInput, 'passwordInvalidError'); - - if (!passwordValue) { - showError(passwordInput, 'passwordEmptyError'); - } else if (passwordValue.length < 8) { - showError(passwordInput, 'passwordInvalidError'); - } else { - isPasswordValid = true; - hideError(passwordInput, 'passwordEmptyError'); - hideError(passwordInput, 'passwordInvalidError'); - } - updateSubmitButtonState(); - - // 비밀번호 입력 전에 비밀번호 확인 필드 입력을 먼저 시도하는 경우를 대비해 검증 로직 강화 - if (signupForm) { - checkPasswordConfirmationValidity(); - } - } - - // 비밀번호 확인 필드의 유효성 검사 - function checkPasswordConfirmationValidity() { - const passwordConfirmationValue = passwordConfirmationInput.value.trim(); - isPasswordConfirmationValid = false; - - hideError(passwordConfirmationInput, 'passwordConfirmationError'); - hideError(passwordConfirmationInput, 'passwordConfirmationInitError'); - - if (!isPasswordValid) { - showError(passwordConfirmationInput, 'passwordConfirmationInitError'); - } else if ( - !passwordConfirmationValue || - passwordConfirmationValue !== passwordInput.value.trim() - ) { - showError(passwordConfirmationInput, 'passwordConfirmationError'); - } else { - isPasswordConfirmationValid = true; - hideError(passwordConfirmationInput, 'passwordConfirmationError'); - hideError(passwordConfirmationInput, 'passwordConfirmationInitError'); - } - updateSubmitButtonState(); - } - - function updateSubmitButtonState() { - // form submit button의 활성화 여부를 관장하기 위한 변수 - let isFormValid = isEmailValid && isPasswordValid; - - if (signupForm) { - isFormValid = - isFormValid && isNicknameValid && isPasswordConfirmationValid; - } - - // isFormValid의 boolean 값에 따라 선택된 제출 버튼의 disabled 속성을 변경 - submitButton.disabled = !isFormValid; - } - - // 입력 필드에 이벤트 리스너 추가 - // - 회원가입 및 로그인 form에서는 사용자가 입력한 데이터의 유효성을 즉각적으로 검증하고 피드백을 제공하기 위해서 focusout, input, change 등의 input event를 많이 사용해요. - if (emailInput) { - // - 입력 필드 선택 후 focus out 했을 때 각 필드에 해당하는 유효성 검증 함수를 호출 - emailInput.addEventListener('focusout', checkEmailValidity); - } - if (nicknameInput) { - nicknameInput.addEventListener('focusout', checkNicknameValidity); - } - if (passwordInput) { - // 로그인에서는 비밀번호 필드가 마지막 항목이므로 입력 후 바로 제출 버튼을 활성화해주려면 focusout보다 input이 더 적절할 것 같아요. - passwordInput.addEventListener('input', checkPasswordValidity); - } - if (passwordConfirmationInput) { - // 비밀번호 확인 필드 입력 시 정상적인 비밀번호 입력값이 있는지, 그리고 두 값이 일치하는지 여부를 실시간으로 확인하고 오류 메세지를 표시하기 위해 focusout이 아닌 input을 추천 - passwordConfirmationInput.addEventListener( - 'input', - checkPasswordConfirmationValidity, - ); - } - - // 페이지 로드 시 제출 버튼의 비활성화 상태를 설정 - updateSubmitButtonState(); - - if (loginForm) { - loginForm.addEventListener('submit', function (event) { - event.preventDefault(); // 기본 제출 동작 방지 - window.location.href = 'items.html'; - }); - } - - if (signupForm) { - signupForm.addEventListener('submit', function (event) { - event.preventDefault(); - window.location.href = 'signup.html'; - }); - } - - // 비밀번호 표시 상태 온오프(toggle) 버튼 동작 - function togglePasswordVisibility(event) { - // 이벤트가 발생한 버튼을 기준으로 타겟 요소를 찾음 - const button = event.currentTarget; - const inputField = button.parentElement.querySelector('input'); - const toggleIcon = button.querySelector('.password-toggle-icon'); - - // input의 type을 'password'으로 설정하면 입력값이 마스킹된 상태(비밀번호 숨김 상태)로 렌더링되고, 'text'로 설정하면 일반 문자로 출력됩니다. - const isPasswordVisible = inputField.type === 'text'; - - // 비밀번호 입력 필드 타입 토글 - // - 비밀번호 필드의 type 설정값이 'password'로 확인되면 'text'로, 'text'라면 반대로 'password'로 업데이트해 준다면 원하는 동작을 구현할 수 있어요. - inputField.type = isPasswordVisible ? 'text' : 'password'; - - // 토글 버튼 아이콘의 이미지 파일과 alt도 비밀번호 표시 상태와 함께 변경해 주세요. - toggleIcon.src = isPasswordVisible - ? 'images/icons/eye-visible.svg' - : 'images/icons/eye-invisible.svg'; - toggleIcon.alt = isPasswordVisible - ? '비밀번호 표시 상태 아이콘' - : '비밀번호 숨김 상태 아이콘'; - - // 버튼의 aria-label 속성 업데이트 - // - 텍스트 정보 없이 이미지로만 되어 있는 버튼 요소이므로 시각장애인의 웹 접근성을 위해 `aria-label`을 추가해 주세요. - // - 버튼 클릭 시 일어나는 동작을 기준으로 설명을 작성해 주세요. - // - `aria-label`은 이름의 hyphen 때문에 자바스크립트에서 유효한 식별자로 인식되지 않아 dot notation을 통해 객체의 프로퍼티에 접근할 수 없어요. - // 이런 속성을 설정하거나 가져올 때는 setAttribute과 getAttribute 메서드를 사용해야 해요. - button.setAttribute( - 'aria-label', - isPasswordVisible ? '비밀번호 숨기기' : '비밀번호 보기', - ); - } - - // 비밀번호 토글 버튼에 이벤트 리스너 추가 - // - 회원가입 페이지에서는 비밀번호 토글 버튼이 두 개이기 때문에 ID 대신 querySelectorAll과 class 선택자를 사용하여 비밀번호 토글 버튼의 배열을 생성한 다음, forEach 루프를 사용해 각 버튼에 클릭 이벤트 리스너를 추가하는 방식을 택했어요. - // - 이 방법은 페이지 내에서 동일한 클래스를 가진 여러 요소에 같은 기능을 적용할 때 효과적이에요. - // - 버튼 클릭 시 실행되는 togglePassword 함수에서는 이벤트가 발생한 특정 버튼에 대한 조작이 이루어지므로, 같은 페이지에 여러 토글 버튼이 있더라도 각각 독립적으로 기능하게 됩니다. - const toggleButtons = document.querySelectorAll('.password-toggle-button'); // 'password-toggle-button' 클래스를 가진 모든 요소들의 배열 - toggleButtons.forEach((button) => - button.addEventListener('click', togglePasswordVisibility), - ); -}); diff --git a/mission_template_codes/mission4-template-code/signup.html b/mission_template_codes/mission4-template-code/signup.html deleted file mode 100644 index 3a6ca982..00000000 --- a/mission_template_codes/mission4-template-code/signup.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - 판다마켓 - 회원가입 - - - - - - - -
- 판다마켓 로고 - - -
-
- - - - - 이메일을 입력해 주세요 - 잘못된 이메일 형식입니다 -
- -
- - - 닉네임을 입력해 주세요 -
- -
- -
- - -
- 비밀번호를 입력해 주세요 - 비밀번호를 8자 이상 입력해 주세요 -
- -
- -
- - -
- 먼저 조건에 맞는 비밀번호를 입력해 주세요 - 비밀번호가 일치하지 않습니다 -
- - - -
- - - -
- 이미 회원이신가요? 로그인 -
-
- - - - - diff --git a/mission_template_codes/mission4-template-code/styles/auth.css b/mission_template_codes/mission4-template-code/styles/auth.css deleted file mode 100644 index a5277157..00000000 --- a/mission_template_codes/mission4-template-code/styles/auth.css +++ /dev/null @@ -1,143 +0,0 @@ -/* Mobile styles */ - -.auth-container { - /* 로고 위 여백은 전체적인 레이아웃으로 보는 것이 적절한 것 같아 로고 요소가 아닌 페이지 전체 컨테이너의 vertical padding으로 적용했어요 */ - /* 컨테이너에 horizontal padding으로 여백을 주고 내부 콘텐츠가 컨테이너의 가로 폭을 꽉 채우도록 하면 화면 크기 변화에 따라 자식 요소들의 크기가 조정되는 효과를 만듭니다 */ - padding: 24px 16px; - /* - 프로젝트 시작할 때 global style에서 전체 요소에 box-sizing: border-box를 적용했던 것 기억하시나요? - 이 설정은 요소의 width에 padding과 border 등의 너비를 포함하도록 만들어 콘텐츠가 레이아웃 밖으로 삐져나오는 것을 방지합니다. - 컨테이너에 padding을 준 상태에서 내부 콘텐츠의 실제 사용 가능 너비를 400px로 유지하고자 한다면 max-width를 400px이 아닌, 좌우 패딩을 고려한 432px (400px + 16px*2)로 설정해 주세요. - */ - max-width: 432px; - margin: 0 auto; -} - -.logo-home-link { - /* inline 요소인 anchor 태그에 margin을 적용하려면 block으로 바꿔야 해요 */ - display: block; - margin-bottom: 24px; - /* inline 자식요소인 img를 가운데 정렬할 때는 text-align: center를 사용하세요 */ - text-align: center; -} - -/* 반응형으로 이미지 크기를 변경하고 싶다면 태그에 바로 width 속성을 넣는 대신 스타일시트에서 img 선택자를 통해 너비를 적용할 수 있어요 */ -.logo-home-link img { - width: 198px; -} - -.input-item { - margin-bottom: 24px; -} - -.input-item label { - display: block; - font-weight: 700; - font-size: 14px; - margin-bottom: 8px; -} - -.input-item input { - padding: 16px 24px; - background-color: #f3f4f6; - border: none; - border-radius: 12px; - font-size: 16px; - line-height: 24px; - width: 100%; -} - -.input-item input::placeholder { - color: #9ca3af; - font-size: 16px; - line-height: 24px; -} - -.input-item input:focus { - outline-color: var(--blue); -} - -.input-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.error-message { - color: #f74747; - font-weight: 600; - font-size: 15px; - line-height: 18px; - margin-top: 8px; - display: none; - padding-left: 16px; -} - -.password-toggle-button { - position: absolute; - right: 24px; -} - -.social-login-container { - background-color: #e6f2ff; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 23px; - margin: 24px 0; -} - -.social-login-container h3 { - font-weight: 500; - font-size: 16px; - line-height: 24px; -} - -.social-login-links-container { - display: flex; - gap: 16px; -} - -.auth-switch { - font-weight: 500; - font-size: 15px; - text-align: center; -} - -.auth-switch a { - color: #3182f6; - text-decoration: underline; - text-underline-offset: 2px; -} - -/* Tablet styles */ - -@media (min-width: 768px) { - .auth-container { - max-width: 640px; - /* max-width 덕분에 이미 가로 여백이 확보되어 있으니, 불필요한 여백이 추가되어 내부 콘텐츠의 너비가 줄어들지 않도록 가로 패딩은 0으로 적용해 주세요 */ - padding: 48px 0; - } - - .logo-home-link { - margin-bottom: 40px; - } - - .logo-home-link img { - width: 396px; - } - - .input-item label { - font-size: 18px; - margin-bottom: 16px; - } -} - -/* Desktop styles */ - -@media (min-width: 1280px) { - .auth-container { - padding: 60px 0; - } -} diff --git a/mission_template_codes/mission4-template-code/styles/global.css b/mission_template_codes/mission4-template-code/styles/global.css deleted file mode 100644 index 782a53c8..00000000 --- a/mission_template_codes/mission4-template-code/styles/global.css +++ /dev/null @@ -1,204 +0,0 @@ -/* Mobile styles */ - -:root { - /* Gray scale */ - --gray-900: #1b1d1f; - --gray-800: #26282b; - --gray-600: #454c53; - --gray-500: #72787f; - --gray-400: #9ea4a8; - --gray-200: #e5e7eb; - --gray-100: #e8ebed; - --gray-50: #f7f7f8; - - /* Primary color */ - --blue: #3692ff; - - /* Layout dimensions */ - --header-height: 70px; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -a { - text-decoration: none; - color: inherit; -} - -button, -input, -textarea, -select { - font-family: inherit; - font-size: inherit; - line-height: inherit; - color: inherit; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -button { - background: none; - border: none; - outline: none; - box-shadow: none; - cursor: pointer; -} - -img { - vertical-align: bottom; -} - -body { - color: #374151; - word-break: keep-all; - font-family: "Pretendard", sans-serif; -} - -header { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: var(--header-height); - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 16px; - background-color: #ffffff; - border-bottom: 1px solid #dfdfdf; -} - -.with-header { - margin-top: var(--header-height); -} - -footer { - background-color: #111827; - color: #9ca3af; - font-size: 16px; - padding: 32px; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 60px; -} - -#copyright { - order: 3; - flex-basis: 100%; -} - -#footerMenu { - display: flex; - gap: 30px; - color: var(--gray-200); -} - -#socialMedia { - display: flex; - gap: 12px; -} - -.wrapper { - width: 100%; - padding: 0 16px; -} - -h1 { - font-size: 40px; - font-weight: 700; - line-height: 56px; - letter-spacing: 0.02em; -} - -.button { - background-color: var(--blue); - color: #ffffff; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.button:hover { - background-color: #1967d6; -} - -.button:focus { - background-color: #1251aa; -} - -.button:disabled { - background-color: #9ca3af; - cursor: default; - pointer-events: none; -} - -.pill-button { - font-size: 16px; - font-weight: 600; - border-radius: 999px; - padding: 14.5px 33.5px; -} - -.full-width { - width: 100%; -} - -.break-on-desktop { - display: none; -} - -/* Tablet styles */ - -@media (min-width: 768px) { - header { - padding: 0 24px; - } - - .wrapper { - padding: 0 24px; - } - - .pill-button { - font-size: 20px; - font-weight: 700; - padding: 16px 126px; - } - - footer { - padding: 32px 104px 108px 104px; - } - - #copyright { - flex-basis: auto; - order: 0; - } -} - -/* Desktop styles */ - -@media (min-width: 1280px) { - header { - padding: 0 200px; - } - - .wrapper { - max-width: 1200px; - margin: 0 auto; - } - - .break-on-desktop { - display: inline; - } - - footer { - padding: 32px 200px 108px 200px; - } -} diff --git a/mission_template_codes/mission4-template-code/styles/home.css b/mission_template_codes/mission4-template-code/styles/home.css deleted file mode 100644 index 94479326..00000000 --- a/mission_template_codes/mission4-template-code/styles/home.css +++ /dev/null @@ -1,170 +0,0 @@ -/* Mobile styles */ - -.banner { - background-color: #cfe5ff; - height: 60vh; - text-align: center; - background-repeat: no-repeat; - background-position: bottom; - background-size: 130%; -} - -#hero { - background-image: url("../images/home/hero-image.png"); -} - -.banner h1 { - font-weight: 700; - font-size: 32px; - line-height: 44.8px; - padding-top: 48px; - padding-bottom: 18px; -} - -#bottomBanner { - background-image: url("../images/home/bottom-banner-image.png"); -} - -#loginLink { - font-size: 16px; - font-weight: 600; - border-radius: 8px; - padding: 11.5px 23px; -} - -#features { - padding-top: 51px; -} - -.feature { - margin-bottom: 64px; -} - -.feature img { - width: 100%; - margin-bottom: 20px; -} - -.feature:nth-child(2) { - text-align: right; -} - -.feature-content { - flex: 1; -} - -.feature-content h2 { - color: var(--blue); - font-size: 16px; - line-height: 22.4px; - font-weight: 700; - margin-bottom: 8px; -} - -.feature-content h1 { - font-weight: 700; - font-size: 24px; - line-height: 33.6px; -} - -.feature-description { - font-weight: 500; - font-size: 16px; - line-height: 19.2px; - letter-spacing: 0.08em; - margin-top: 20px; -} - -/* Tablet styles */ - -@media (min-width: 768px) { - .banner { - height: 90vh; - background-size: 120%; - } - - .banner h1 { - font-size: 40px; - line-height: 56px; - padding-top: 84px; - padding-bottom: 24px; - } - - #hero h1 br { - display: none; - } - - #features { - padding-top: 24px; - padding-bottom: 16px; - } - - .feature-content h2 { - font-size: 18px; - line-height: 25.2px; - margin-bottom: 12px; - } - - .feature-content h1 { - font-size: 32px; - line-height: 44.8px; - } - - .feature-description { - font-size: 18px; - line-height: 21.6px; - } -} - -/* Desktop styles */ - -@media (min-width: 1280px) { - .banner { - text-align: left; - height: 540px; - display: flex; - align-items: center; - background-position: 80% bottom; - background-size: 55%; - } - - .banner h1 { - padding-top: 0; - padding-bottom: 32px; - } - - #hero h1 br { - display: inline; - } - - #features { - padding: 138px 0; - } - - .feature { - margin-bottom: 138px; - display: flex; - align-items: center; - gap: 5%; - } - - .feature:nth-child(2) { - flex-direction: row-reverse; - } - - .feature img { - width: 50%; - margin-bottom: 0; - } - - .feature-content h1 { - font-size: 40px; - line-height: 56px; - } - - .feature-description { - font-size: 24px; - line-height: 28.8px; - margin-top: 24px; - } -} diff --git a/src/App.jsx b/src/App.jsx index 53a63682..ebc38698 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,16 +1,21 @@ import { Outlet, useLocation } from 'react-router-dom'; -import { Header, Footer } from './components/common'; +import { Header, Footer } from '@/components/common'; import layoutStyles from '@/styles/layout/layout.module.scss'; function App() { const location = useLocation(); const isAuthPage = ['/signin', '/signup'].includes(location.pathname); + const isAuthOrLanding = ['/', '/signin', '/signup'].includes( + location.pathname, + ); return (
{!isAuthPage &&
}
- +
+ +
{!isAuthPage &&
}
diff --git a/src/components/AddItem/AddItemForm/AddItemForm.jsx b/src/components/AddItem/AddItemForm/AddItemForm.jsx new file mode 100644 index 00000000..07e845fc --- /dev/null +++ b/src/components/AddItem/AddItemForm/AddItemForm.jsx @@ -0,0 +1,63 @@ +import formStyles from '@/styles/helpers/formHelpers.module.scss'; + +const AddItemForm = ({ formData, handleInputChange }) => { + return ( + <> +
+ + + handleInputChange({ field: 'productName', value: e.target.value }) + } + /> +
+ +
+ +