+
diff --git a/src/pages/Items/Items.module.scss b/src/pages/Items/Items.module.scss
deleted file mode 100644
index 7703b125..00000000
--- a/src/pages/Items/Items.module.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.itemsPage {
- margin: 0 auto;
- max-width: 120rem; // header content 110일땐 80rem이었는데 120으로 수정
- padding: 24px 0;
-}
-@media (max-width: 1919px) {
- .itemsPage {
- max-width: 100%;
- padding: 24px 250px;
- }
-}
-
-@media (max-width: 1199px) {
- .itemsPage {
- padding: 24px 24px;
- }
-}
-
-@media (max-width: 767px) {
- .itemsPage {
- padding: 24px 16px;
- }
-}
diff --git a/src/pages/SignIn/SignIn.jsx b/src/pages/SignIn/SignIn.jsx
index 218067be..0b0d35d4 100644
--- a/src/pages/SignIn/SignIn.jsx
+++ b/src/pages/SignIn/SignIn.jsx
@@ -1,16 +1,15 @@
import { useNavigate } from 'react-router-dom';
-import { useForm } from '@/hooks';
+import { useAuthForm } from '@/hooks';
import { AuthFormLayout } from '@/components/auth';
-import { signInValidationRules } from '@/utils/validators';
+import { signInValidation } from '@/utils/validators';
import { ROUTES } from '@/constants/urls';
-const initialFormData = {
- email: '',
- password: '',
-};
-
const SignIn = () => {
const navigate = useNavigate();
+ const initialFormData = {
+ email: '',
+ password: '',
+ };
const {
formData,
setFormData,
@@ -20,7 +19,7 @@ const SignIn = () => {
setShowPasswordStates,
isFormValid,
handleInputChange,
- } = useForm(initialFormData, signInValidationRules);
+ } = useAuthForm(initialFormData, signInValidation);
const handleSubmit = (e) => {
e.preventDefault();
@@ -37,7 +36,6 @@ const SignIn = () => {
errors,
setErrors,
onSubmit: handleSubmit,
- validationRules: signInValidationRules,
showPasswordStates,
togglePasswordVisibility: setShowPasswordStates,
isFormValid,
diff --git a/src/pages/SignUp/SignUp.jsx b/src/pages/SignUp/SignUp.jsx
index 2231dd7d..a50cc83e 100644
--- a/src/pages/SignUp/SignUp.jsx
+++ b/src/pages/SignUp/SignUp.jsx
@@ -1,18 +1,17 @@
import { useNavigate } from 'react-router-dom';
-import { useForm } from '@/hooks';
+import { useAuthForm } from '@/hooks';
import { AuthFormLayout } from '@/components/auth';
-import { signUpValidationRules } from '@/utils/validators';
+import { signUpValidation } from '@/utils/validators';
import { ROUTES } from '@/constants/urls';
-const initialFormData = {
- email: '',
- nickname: '',
- password: '',
- confirmPassword: '',
-};
-
const SignUp = () => {
const navigate = useNavigate();
+ const initialFormData = {
+ email: '',
+ nickname: '',
+ password: '',
+ confirmPassword: '',
+ };
const {
formData,
setFormData,
@@ -22,7 +21,7 @@ const SignUp = () => {
setShowPasswordStates,
isFormValid,
handleInputChange,
- } = useForm(initialFormData, signUpValidationRules);
+ } = useAuthForm(initialFormData, signUpValidation);
const handleSubmit = (e) => {
e.preventDefault();
@@ -39,7 +38,6 @@ const SignUp = () => {
errors,
setErrors,
onSubmit: handleSubmit,
- validationRules: signUpValidationRules,
showPasswordStates,
togglePasswordVisibility: setShowPasswordStates,
isFormValid,
diff --git a/src/pages/index.js b/src/pages/index.js
index 39e9e724..216545e5 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -2,3 +2,4 @@ export { default as Landing } from './Landing';
export { default as SignUp } from './SignUp';
export { default as SignIn } from './SignIn';
export { default as Items } from './Items';
+export { default as AddItem } from './AddItem';
diff --git a/src/routes/index.jsx b/src/routes/index.jsx
new file mode 100644
index 00000000..37838232
--- /dev/null
+++ b/src/routes/index.jsx
@@ -0,0 +1,17 @@
+import { Route, Routes } from 'react-router-dom';
+import { Landing, SignUp, SignIn, Items, AddItem } from '@/pages';
+import App from '@/App';
+
+const AppRoutes = () => (
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+);
+
+export default AppRoutes;
diff --git a/src/styles/common/base.css b/src/styles/common/base.css
index 68429080..5885e0e8 100644
--- a/src/styles/common/base.css
+++ b/src/styles/common/base.css
@@ -1,12 +1,13 @@
html {
font-size: 10px; /* 1rem = 10px */
+ word-break: keep-all;
}
body {
font-size: 1rem;
font-family: 'Pretendard', sans-serif;
background-color: var(--secondary-300);
- color: var(--black);
+ color: var(--secondary-800);
}
p {
@@ -24,28 +25,12 @@ select {
cursor: pointer;
}
-button,
-.button {
- background-color: var(--primary-100);
- color: var(--white);
+button {
cursor: pointer;
border: none;
-}
-
-button:hover,
-.button:hover {
- background-color: var(--primary-200);
-}
-
-button:active,
-.button:active {
- background-color: var(--primary-300);
-}
-
-button:disabled,
-.button:disabled {
- background-color: var(--secondary-400);
- cursor: not-allowed;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
}
@media (max-width: 1919px) {
diff --git a/src/styles/common/reset.css b/src/styles/common/reset.css
index c737afe5..aac599b6 100644
--- a/src/styles/common/reset.css
+++ b/src/styles/common/reset.css
@@ -12,7 +12,8 @@ h5,
h6,
p,
ul,
-ol {
+ol,
+label {
margin: 0;
}
@@ -39,6 +40,10 @@ select {
padding: 0;
}
+textarea {
+ resize: none;
+}
+
input[type='checkbox'],
input[type='radio'],
select,
diff --git a/src/styles/common/variables.css b/src/styles/common/variables.css
index dd4f7415..6b9e16ef 100644
--- a/src/styles/common/variables.css
+++ b/src/styles/common/variables.css
@@ -21,5 +21,17 @@
--secondary-800: #1f2937;
--secondary-900: #111827; /* footer bg*/
+ --primary-bg: rgba(225, 245, 254, 0.9);
--error: #f74747;
+ --error-bg: rgba(253, 236, 234, 0.95); /* error message bg */
+ --success: #2fd172;
+ --success-bg: rgba(237, 247, 237, 0.9);
+ --focus-ring: rgba(
+ 54,
+ 146,
+ 255,
+ 0.3
+ ); /* primary-100을 바탕으로 한 반투명 색 */
+
+ --shadow-color: rgba(0, 0, 0, 0.2);
}
diff --git a/src/styles/helpers/buttonHelpers.module.scss b/src/styles/helpers/buttonHelpers.module.scss
index 3b533cd1..0ce341b4 100644
--- a/src/styles/helpers/buttonHelpers.module.scss
+++ b/src/styles/helpers/buttonHelpers.module.scss
@@ -1,11 +1,22 @@
-.viewButton {
- padding: 1.1rem 10rem;
- border-radius: 40px;
- font-size: 1.4rem;
-}
+.primary {
+ background-color: var(--primary-100);
+ color: var(--secondary-100);
+ padding: 1rem 1.6rem;
+ border-radius: 8px;
+ border: none;
+ font-weight: 600;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: var(--primary-200);
+ }
+
+ &:active {
+ background-color: var(--primary-300);
+ }
-@media (max-width: 767px) {
- .viewButton {
- padding: 1.1rem 8rem;
+ &:disabled {
+ background-color: var(--secondary-400);
+ cursor: not-allowed;
}
}
diff --git a/src/styles/helpers/formHelpers.module.scss b/src/styles/helpers/formHelpers.module.scss
new file mode 100644
index 00000000..52f65832
--- /dev/null
+++ b/src/styles/helpers/formHelpers.module.scss
@@ -0,0 +1,47 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 2.4rem;
+}
+
+.inputContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+
+.labelText {
+ font-size: 1.8rem;
+ font-weight: 700;
+ line-height: 2.6rem;
+}
+
+.input,
+.textarea {
+ padding: 1.6rem 2.4rem 1.4rem 2.4rem;
+ border-radius: 12px;
+ background-color: var(--secondary-100);
+ font-size: 1.6rem;
+ font-weight: 400;
+ width: 100%;
+
+ &:focus {
+ outline: none;
+ border-color: var(--secondary-800);
+ box-shadow: 0 0 0 2px var(--focus-ring);
+ }
+}
+
+.validationErrorMessage {
+ color: var(--error);
+ font-size: 1.4rem;
+ font-weight: 600;
+ line-height: 2.4rem;
+ display: none;
+
+ &.active {
+ display: block;
+ margin-top: -8px;
+ padding: 0.8rem 1.6rem;
+ }
+}
diff --git a/src/styles/layout/layout.module.scss b/src/styles/layout/layout.module.scss
index 44ace8df..6f880443 100644
--- a/src/styles/layout/layout.module.scss
+++ b/src/styles/layout/layout.module.scss
@@ -1,3 +1,28 @@
.layoutWrapper {
margin: 0 auto;
}
+
+.pageWrapper {
+ margin: 0 auto;
+ max-width: 120rem; // header content 110일땐 80rem이었는데 120으로 수정
+ padding: 24px 0;
+}
+
+@media (max-width: 1919px) {
+ .pageWrapper {
+ max-width: 100%;
+ padding: 24px 250px;
+ }
+}
+
+@media (max-width: 1199px) {
+ .pageWrapper {
+ padding: 24px 24px;
+ }
+}
+
+@media (max-width: 767px) {
+ .pageWrapper {
+ padding: 24px 16px;
+ }
+}
diff --git a/src/utils/api/index.js b/src/utils/api/index.js
new file mode 100644
index 00000000..65130f3d
--- /dev/null
+++ b/src/utils/api/index.js
@@ -0,0 +1,2 @@
+export { default as safeFetch } from './safeFetch';
+// export { default as handleApiError } from './handleApiError';
diff --git a/src/utils/api/safeFetch.js b/src/utils/api/safeFetch.js
new file mode 100644
index 00000000..a86809aa
--- /dev/null
+++ b/src/utils/api/safeFetch.js
@@ -0,0 +1,59 @@
+import HTTP_STATUS from '@/constants/statusCodes';
+import { HTTP_ERROR_MESSAGES } from '@/constants/messages';
+
+const safeFetch = async ({
+ url,
+ options,
+ showToast,
+ showToastOnError = true,
+ uiErrorMessage = HTTP_ERROR_MESSAGES.UNKNOWN,
+}) => {
+ try {
+ const res = await fetch(url, options);
+
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ const message = errorData.message || uiErrorMessage;
+ const error = new Error(message);
+ error.status = res.status;
+ throw error;
+ }
+
+ return await res.json();
+ } catch (error) {
+ // 콘솔 에러: 개발자용
+ console.error(
+ `🔴 요청 에러 [${error.status ?? 'unknown'}]:`,
+ error.message,
+ );
+
+ // 에러 메시지: 사용자용
+ if (showToast && showToastOnError) {
+ let toastMessage = '';
+
+ switch (error.status) {
+ case HTTP_STATUS.UNAUTHORIZED:
+ toastMessage = HTTP_ERROR_MESSAGES.UNAUTHORIZED;
+ break;
+ case HTTP_STATUS.FORBIDDEN:
+ toastMessage = HTTP_ERROR_MESSAGES.FORBIDDEN;
+ break;
+ case HTTP_STATUS.NOT_FOUND:
+ toastMessage = HTTP_ERROR_MESSAGES.NOT_FOUND;
+ break;
+ default:
+ if (error.status >= 500) {
+ toastMessage = HTTP_ERROR_MESSAGES.SERVER_ERROR;
+ } else {
+ toastMessage = error.message || uiErrorMessage;
+ }
+ }
+
+ showToast(toastMessage, 'error');
+ }
+
+ throw error;
+ }
+};
+
+export default safeFetch;
diff --git a/src/utils/validators/addItemValidation.js b/src/utils/validators/addItemValidation.js
new file mode 100644
index 00000000..13a293be
--- /dev/null
+++ b/src/utils/validators/addItemValidation.js
@@ -0,0 +1,12 @@
+const addItemValidation = {
+ imageFile: (file) => Boolean(file),
+ productName: (value) => Boolean(value.trim()),
+ description: (value) => Boolean(value.trim()),
+ price: (value) => {
+ const num = Number(value);
+ return Boolean(value) && !isNaN(num);
+ },
+ tags: (arr) => Array.isArray(arr) && arr.length > 0,
+};
+
+export default addItemValidation;
diff --git a/src/utils/validators/authValidation.js b/src/utils/validators/authValidation.js
new file mode 100644
index 00000000..b1b54fe3
--- /dev/null
+++ b/src/utils/validators/authValidation.js
@@ -0,0 +1,40 @@
+import { AUTH_ERROR_MESSAGES } from '@/constants/messages';
+import { EMAIL_REGEX } from '@/constants/regex.js';
+
+const validateEmailFormat = (email) => EMAIL_REGEX.test(email);
+
+const validateEmail = (value) => {
+ if (!value) return AUTH_ERROR_MESSAGES.emailRequired;
+ if (!validateEmailFormat(value)) return AUTH_ERROR_MESSAGES.invalidEmail;
+ return '';
+};
+
+const validateNickname = (value) => {
+ if (!value) return AUTH_ERROR_MESSAGES.nicknameRequired;
+ return '';
+};
+
+const validatePassword = (value) => {
+ if (!value) return AUTH_ERROR_MESSAGES.passwordRequired;
+ if (value.length < 8) return AUTH_ERROR_MESSAGES.passwordLength;
+ return '';
+};
+
+const validateConfirmPassword = (value, allFormValues) => {
+ if (!value) return AUTH_ERROR_MESSAGES.confirmPasswordRequired;
+ if (value !== allFormValues.password)
+ return AUTH_ERROR_MESSAGES.passwordMismatch;
+ return '';
+};
+
+export const signUpValidation = {
+ email: validateEmail,
+ nickname: validateNickname,
+ password: validatePassword,
+ confirmPassword: validateConfirmPassword,
+};
+
+export const signInValidation = {
+ email: validateEmail,
+ password: validatePassword,
+};
diff --git a/src/utils/validators/index.js b/src/utils/validators/index.js
index 77ddd9f4..380cda2e 100644
--- a/src/utils/validators/index.js
+++ b/src/utils/validators/index.js
@@ -1,4 +1,2 @@
-export {
- signUpValidationRules,
- signInValidationRules,
-} from './validationRules';
+export { signUpValidation, signInValidation } from './authValidation';
+export { default as addItemValidation } from './addItemValidation';
diff --git a/src/utils/validators/validationRules.js b/src/utils/validators/validationRules.js
deleted file mode 100644
index 57bf3e6e..00000000
--- a/src/utils/validators/validationRules.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ERROR_MESSAGES } from '@/constants/messages';
-import { EMAIL_REGEX } from '@/constants/regex.js';
-
-const validateEmailFormat = (email) => EMAIL_REGEX.test(email);
-
-const validateEmail = (value) => {
- if (!value) return ERROR_MESSAGES.emailRequired;
- if (!validateEmailFormat(value)) return ERROR_MESSAGES.invalidEmail;
- return '';
-};
-
-const validateNickname = (value) => {
- if (!value) return ERROR_MESSAGES.nicknameRequired;
- return '';
-};
-
-const validatePassword = (value) => {
- if (!value) return ERROR_MESSAGES.passwordRequired;
- if (value.length < 8) return ERROR_MESSAGES.passwordLength;
- return '';
-};
-
-const validateConfirmPassword = (value, allFormValues) => {
- if (!value) return ERROR_MESSAGES.confirmPasswordRequired;
- if (value !== allFormValues.password) return ERROR_MESSAGES.passwordMismatch;
- return '';
-};
-
-export const signUpValidationRules = {
- email: validateEmail,
- nickname: validateNickname,
- password: validatePassword,
- confirmPassword: validateConfirmPassword,
-};
-
-export const signInValidationRules = {
- email: validateEmail,
- password: validatePassword,
-};