diff --git a/package.json b/package.json
index bad50a9..d7f78b8 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"axios": "^1.4.0",
"bcrypt": "^5.1.0",
"bull": "^4.10.4",
+ "cheerio": "^1.0.0-rc.12",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
@@ -31,6 +32,7 @@
"mongoose": "^7.3.0",
"multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1",
+ "nodemailer": "^6.9.3",
"passport": "^0.6.0",
"passport-local": "^1.0.0"
}
diff --git a/src/main.js b/src/main.js
index b71a60d..d6997f1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -18,7 +18,9 @@ const aiAPI = process.env.AI_API
const allowedOrigins = [
'http://localhost:3000',
- 'http://www.model-fit.kro.kr',
+ 'http://localhost:3001',
+ process.env.WEB_API,
+ process.env.WEB_IP,
aiAPI,
]
@@ -60,9 +62,6 @@ app.use(express.static(path.join(__dirname, '../style/build')))
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../style/build/index.html'))
})
-app.get('*', (req, res) => {
- res.sendFile(path.join(__dirname, '../style/build/index.html'))
-})
const usersRouter = require('./routes/RegisterUser')
const loginRouter = require('./routes/LoginUser')
@@ -70,3 +69,24 @@ const logoutRouter = require('./routes/LogoutUser')
app.use('/users', usersRouter)
app.use('/users', loginRouter)
app.use('/users', logoutRouter)
+
+const authRouter = require('./routes/MailAuthRouter')
+app.use('/auth', authRouter)
+
+// userInfo 조회
+const infoRouter = require('./routes/UserInfo')
+app.use('/userInfo', infoRouter)
+
+const fittingRouter = require('./routes/FittingImage')
+const clothUploadRouter = require('./routes/UploadClothImage')
+app.use('/api', fittingRouter)
+app.use('/api', clothUploadRouter)
+
+const clothRouter = require('./routes/Clothes')
+const clothSizeRouter = require('./routes/ClothSize')
+app.use('/cloth', clothRouter)
+app.use('/cloth', clothSizeRouter)
+
+app.get('*', (req, res) => {
+ res.sendFile(path.join(__dirname, '../style/build/index.html'))
+})
diff --git a/src/models/Closet.js b/src/models/Closet.js
new file mode 100644
index 0000000..85228dc
--- /dev/null
+++ b/src/models/Closet.js
@@ -0,0 +1,11 @@
+const mongoose = require('mongoose')
+const { Schema } = mongoose
+
+const closetSchema = new Schema({
+ userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
+ clothesUrl: { type: String, required: true },
+ clothesImageLink: { type: String, required: true },
+ fittingImageLink: { type: String },
+})
+
+module.exports = mongoose.model('Closet', closetSchema)
diff --git a/src/models/SizeProfile.js b/src/models/SizeProfile.js
index 8351027..256673d 100644
--- a/src/models/SizeProfile.js
+++ b/src/models/SizeProfile.js
@@ -2,10 +2,10 @@ const mongoose = require('mongoose')
const { Schema } = mongoose
const sizeProfileSchema = new Schema({
- userId: { type: Schema.Types.ObjectId, ref: 'User' },
- length: { type: Number },
- shoulderWidth: { type: Number },
- chestWidth: { type: Number },
+ userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
+ length: { type: Number, required: true },
+ shoulderWidth: { type: Number, required: true },
+ chestWidth: { type: Number, required: true },
})
module.exports = mongoose.model('SizeProfile', sizeProfileSchema)
diff --git a/src/models/User.js b/src/models/User.js
index df14feb..8dabd45 100644
--- a/src/models/User.js
+++ b/src/models/User.js
@@ -9,13 +9,14 @@ const userSchema = new Schema({
gender: { type: String, required: true },
height: { type: Number, required: true },
weight: { type: Number, required: true },
- file: { type: String },
+ file: { type: String, required: true },
favoriteStyle: {
style: { type: String, default: '' },
color: { type: String, default: '' },
fit: { type: String, default: '정핏' },
},
sizeProfile: { type: Schema.Types.ObjectId, ref: 'SizeProfile' },
+ clothes: [{ type: Schema.Types.ObjectId, ref: 'Clothes' }],
})
module.exports = mongoose.model('User', userSchema)
diff --git a/src/modules/AuthMail/AuthMail.js b/src/modules/AuthMail/AuthMail.js
new file mode 100644
index 0000000..3809ef7
--- /dev/null
+++ b/src/modules/AuthMail/AuthMail.js
@@ -0,0 +1,43 @@
+const nodemailer = require('nodemailer')
+const generateAuthCode = require('./generateAuthCode')
+
+/*
+ 구글에서는 from 바꾸는 부분 동작하지 않음
+ 참고 : https://nodemailer.com/usage/using-gmail/
+*/
+const noreply = 'noreply@model-fit.kro.kr'
+
+async function sendAuthMail(email) {
+ try {
+ // SMTP 전송을 위한 transporter 생성
+ const transporter = nodemailer.createTransport({
+ service: 'Gmail',
+ auth: {
+ user: process.env.AUTH_BOT_MAIL,
+ pass: process.env.AUTH_BOT_PASS,
+ },
+ })
+
+ // 이메일 옵션 설정
+ const code = generateAuthCode()
+ const mailOptions = {
+ from: `AuthBot<${noreply}>`,
+ to: email,
+ subject: 'The authorization code for model.fit.',
+ text: `code : ${code}`,
+ }
+
+ // 이메일 전송
+ const info = await transporter.sendMail(mailOptions)
+ console.log(`Email sent successfully : ${email}`)
+ return {
+ authCode: code,
+ result: { success: true, code: 'SEND_DONE', errno: 0 },
+ }
+ } catch (error) {
+ console.error(`Email sent failed : ${email}`)
+ return { success: false, code: error.code, errno: -1 }
+ }
+}
+
+module.exports = sendAuthMail
diff --git a/src/modules/AuthMail/generateAuthCode.js b/src/modules/AuthMail/generateAuthCode.js
new file mode 100644
index 0000000..7943f8d
--- /dev/null
+++ b/src/modules/AuthMail/generateAuthCode.js
@@ -0,0 +1,13 @@
+const crypto = require('crypto')
+
+// 6자리의 숫자로만 구성된 난수 생성
+function generateAuthCode() {
+ const byteLength = 4
+
+ const buffer = crypto.randomBytes(byteLength)
+ const randomCode = parseInt(buffer.toString('hex'), 16).toString().slice(0, 6)
+
+ return randomCode
+}
+
+module.exports = generateAuthCode
diff --git a/src/routes/ClothSize.js b/src/routes/ClothSize.js
new file mode 100644
index 0000000..ad67718
--- /dev/null
+++ b/src/routes/ClothSize.js
@@ -0,0 +1,40 @@
+const express = require('express')
+const router = express.Router()
+const User = require('../models/User')
+const SizeProfile = require('../models/SizeProfile')
+const Closet = require('../models/Closet')
+const axios = require('axios')
+
+router.post('/cloth_size', async (req, res) => {
+ try {
+ const { userId, clothId } = req.body
+
+ const user = await User.findById(userId).populate('sizeProfile')
+ const sizeProfile = user.sizeProfile
+ const closet = await Closet.findById(clothId)
+
+ const length = sizeProfile.length
+ const shoulderWidth = sizeProfile.shoulderWidth
+ const chestWidth = sizeProfile.chestWidth
+ const imageUrl = closet.clothesUrl
+ const overfit = user.favoriteStyle.fit
+
+ const aiApiResponse = await axios.get(process.env.AI_CLOTH_SIZE_API_URL, {
+ params: {
+ length,
+ shoulderWidth,
+ chestWidth,
+ imageUrl,
+ overfit,
+ },
+ })
+
+ const recommendedSize = aiApiResponse.data.size
+ res.json({ size: recommendedSize })
+ } catch (error) {
+ console.error(error)
+ res.status(500).json({ message: 'Server error' })
+ }
+})
+
+module.exports = router
diff --git a/src/routes/Clothes.js b/src/routes/Clothes.js
new file mode 100644
index 0000000..60b9e5a
--- /dev/null
+++ b/src/routes/Clothes.js
@@ -0,0 +1,69 @@
+const express = require('express')
+const router = express.Router()
+const Clothes = require('../models/Closet')
+const User = require('../models/User')
+const { getClothesImageUrls } = require('./crawling')
+const { deleteImageFromS3 } = require('./ImageUploader')
+
+router.get('/api/clothes', async (req, res) => {
+ const { userId } = req.query
+ try {
+ const user = await User.findById(userId)
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: 'The user was not found.' })
+ }
+ const clothes = await Clothes.find({ userId: userId })
+ res.json(
+ clothes.map((cloth) => ({
+ clothesImageLink: cloth.clothesImageLink,
+ _id: cloth._id,
+ }))
+ )
+ } catch (err) {
+ console.error(err.message)
+ res.status(500).send('Server Error')
+ }
+})
+
+// 입력받은 url에서 이미지 링크 검색 후 반환
+router.get('/api/clothesFromUrl', async (req, res) => {
+ const { url } = req.query
+
+ const result = await getClothesImageUrls(url)
+ if (result.success) {
+ console.log('success')
+ return res.status(200).json(result)
+ } else {
+ return res.status(500).json(result)
+ }
+})
+
+router.delete('/api/delete/:id', async (req, res) => {
+ const clothId = req.params.id
+ try {
+ const cloth = await Clothes.findById(clothId)
+ if (!cloth) {
+ return res.status(404).json({ error: 'No cloth found for given id' })
+ }
+ if (cloth.clothesImageLink && cloth.clothesImageLink.trim() !== '') {
+ console.log(cloth.clothesImageLink)
+ await deleteImageFromS3(cloth.clothesImageLink)
+ }
+
+ if (cloth.fittingImageLink && cloth.fittingImageLink.trim() !== '') {
+ console.log(cloth.fittingImageLink)
+ await deleteImageFromS3(cloth.fittingImageLink)
+ }
+
+ await Clothes.findByIdAndDelete(clothId)
+ res
+ .status(200)
+ .json({ message: 'Cloth deleted successfully', code: 'DELETE_DONE' })
+ } catch (error) {
+ res.status(400).json({ error: 'Error while deleting the cloth' })
+ }
+})
+
+module.exports = router
diff --git a/src/routes/FittingImage.js b/src/routes/FittingImage.js
new file mode 100644
index 0000000..b5fc270
--- /dev/null
+++ b/src/routes/FittingImage.js
@@ -0,0 +1,91 @@
+const express = require('express')
+const router = express.Router()
+const axios = require('axios')
+const https = require('https')
+const User = require('../models/User')
+const Clothes = require('../models/Closet')
+const fittingApiUrl = process.env.AI_FITTING_API_URL
+
+async function isImageValid(imageUrl) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ method: 'HEAD',
+ url: imageUrl,
+ }
+
+ const req = https.request(imageUrl, options, (res) => {
+ if (res.statusCode === 200) {
+ resolve(true)
+ } else {
+ resolve(false)
+ }
+ })
+
+ req.on('error', (error) => {
+ console.error(`Error checking image URL: ${imageUrl}`, error)
+ resolve(false)
+ })
+
+ req.end()
+ })
+}
+router.post('/fitting', async (req, res) => {
+ try {
+ const { userId, clothID } = req.body
+
+ // 사용자 이미지 가져오기
+ const user = await User.findOne({ _id: userId })
+ if (!user) {
+ return res.status(404).json({ error: 'User not found' })
+ }
+ const userImageUrl = user.file
+ const cloth = await Clothes.findOne({ _id: clothID })
+ if (!cloth) {
+ return res.status(404).json({ error: 'Cloth not found' })
+ }
+ const clothImageUrl = cloth.clothesImageLink
+
+ // 이미 피팅이미지가 있으면
+ if (cloth && cloth.fittingImageLink) {
+ const isValid = await isImageValid(cloth.fittingImageLink)
+ if (isValid) {
+ return res
+ .status(200)
+ .json({ fittingImageLink: cloth.fittingImageLink })
+ } else {
+ console.warn(
+ 'Stored fittingImageLink is not valid. Fetching new image.'
+ )
+ }
+ }
+ const response = await axios.post(fittingApiUrl, null, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ params: {
+ ID: userId,
+ image_url: userImageUrl,
+ cloth_url: clothImageUrl,
+ },
+ })
+ if (response.data.success) {
+ console.log('Fitting request sent successfully.')
+
+ const imageUrl = response.data.file_name
+ await Clothes.updateOne(
+ { userId, clothesImageLink: clothImageUrl },
+ { $set: { fittingImageLink: imageUrl } }
+ )
+
+ res.status(200).json({ fittingImageLink: imageUrl })
+ } else {
+ console.error('Error:', response.data.error)
+ res.status(500).json({ error: response.data.error })
+ }
+ } catch (error) {
+ console.error('Fitting Request Error:', error)
+ res.status(500).json({ error: 'Fitting Request Error' })
+ }
+})
+
+module.exports = router
diff --git a/src/routes/ImageUploader.js b/src/routes/ImageUploader.js
index cd4b9e3..2905d8a 100644
--- a/src/routes/ImageUploader.js
+++ b/src/routes/ImageUploader.js
@@ -2,8 +2,10 @@ const aws = require('@aws-sdk/client-s3')
const multer = require('multer')
const multers3 = require('multer-s3')
const path = require('path')
+const { ObjectId } = require('mongodb')
+const url = require('url')
-const { S3Client, PutObjectCommand } = aws
+const { S3Client, PutObjectCommand, DeleteObjectCommand } = aws
const s3 = new S3Client({
region: 'us-east-2',
@@ -26,13 +28,32 @@ const ImageUploader = multer({
s3: s3,
bucket: 'bigprogect-bucket',
key: (req, file, callback) => {
- const uploadDirectory = req.query.directory ?? 'images'
+ /*
+ 회원가입 시 신체 이미지 업로드 하면 아직 DB에 컬럼이 없음.
+ => userId 필드는 무조건 undefined가 됨.
+ => s3/undefined/에 이미지가 저장 됨
+ => id를 생성할 수 있게 [ ?? new ObjectId().toString() ] 추가
+ */
+ const userId =
+ (req.body.userId || req.query.userId) ?? new ObjectId().toString()
+ const uploadDirectory = req.query.directory ?? userId
const extension = path.extname(file.originalname)
if (!allowedExtensions.includes(extension)) {
return callback(new Error('wrong extension'))
}
- const userId = req.query.userId
- callback(null, `${uploadDirectory}/${userId}/userimage${extension}`)
+
+ const imageType = req.query.type
+ let imageFilename
+
+ if (imageType === 'body') {
+ imageFilename = `userimage${extension}`
+ } else if (imageType === 'clothing') {
+ imageFilename = file.originalname
+ } else {
+ return callback(new Error('Unknown image type'))
+ }
+
+ callback(null, `${uploadDirectory}/${imageFilename}`)
},
acl: 'public-read-write',
}),
@@ -42,4 +63,20 @@ const ImageUploader = multer({
},
})
-module.exports = ImageUploader
+// 이미지 삭제
+const deleteImageFromS3 = async (imageUrl) => {
+ const imageKey = decodeURIComponent(new URL(imageUrl).pathname.substring(1))
+ const params = {
+ Bucket: 'bigprogect-bucket',
+ Key: imageKey,
+ }
+
+ try {
+ const deleteObject = new DeleteObjectCommand(params)
+ await s3.send(deleteObject)
+ } catch (error) {
+ console.error('Error deleting image from S3:', error)
+ }
+}
+
+module.exports = { ImageUploader, deleteImageFromS3 }
diff --git a/src/routes/MailAuthRouter.js b/src/routes/MailAuthRouter.js
new file mode 100644
index 0000000..a77fabd
--- /dev/null
+++ b/src/routes/MailAuthRouter.js
@@ -0,0 +1,43 @@
+const express = require('express')
+const router = express.Router()
+const sendAuthMail = require('../modules/AuthMail/AuthMail')
+
+const authStore = new Map()
+
+// 인증 저장소에 이메일-인증값 저장
+const addAuth = async (email, authCode) => {
+ authStore.set(email, authCode)
+ await new Promise((resolve) =>
+ setTimeout(resolve, process.env.MAIL_AUTH_TIME)
+ )
+ if (authStore.has(email)) authStore.delete(email)
+}
+
+// 인증 저장소에 저장된 값으로 인증 확인
+const checkAuth = (email, authCode) => {
+ if (!authStore.has(email)) return false
+ else if (authStore.get(email) !== authCode) return false
+ else {
+ authStore.delete(email)
+ return true
+ }
+}
+
+router.post('/api/sendMail', async (req, res) => {
+ const { email } = req.query
+ const { authCode, result } = await sendAuthMail(email)
+ if (result.success) {
+ addAuth(email, authCode)
+ res.status(200).json(result)
+ } else res.status(500).json(result)
+})
+
+router.post('/api/mailAuthCheck', async (req, res) => {
+ const { email, authCode } = req.query
+ if (checkAuth(email, authCode))
+ res.status(200).json({ success: true, code: 'AUTH_CHECK_DONE', errno: 0 })
+ else
+ res.status(200).json({ success: false, code: 'AUTH_CHECK_FAIL', errno: -1 })
+})
+
+module.exports = router
diff --git a/src/routes/RegisterUser.js b/src/routes/RegisterUser.js
index 5fdeeaa..5eea910 100644
--- a/src/routes/RegisterUser.js
+++ b/src/routes/RegisterUser.js
@@ -4,89 +4,130 @@ const bcrypt = require('bcrypt')
const User = require('../models/User')
const SizeProfile = require('../models/SizeProfile')
const jwt = require('jsonwebtoken')
-const ImageUploader = require('./ImageUploader')
+const { ImageUploader } = require('./ImageUploader')
const axios = require('axios')
-
-router.post('/register', ImageUploader.single('file'), async (req, res) => {
- const {
- email: userEmail,
- name,
- phoneNumber,
- password,
- gender,
- height,
- weight,
- favoriteStyle,
- } = req.body
- const file = req.file ? req.file.location : undefined
-
- try {
- const userExists = await User.findOne({ email: userEmail })
-
- if (userExists) {
- return res.status(400).json({ msg: '이미 가입된 이메일입니다.' })
- }
-
- // 비밀번호 해시화
- const hashedPassword = await bcrypt.hash(password, 10)
-
- // 새로운 사용자 생성 및 저장
- const newUser = new User({
+const aiSizeApi = process.env.AI_SIZE_API_URL
+
+router.post(
+ '/register',
+ (req, res, next) => {
+ req.query.type = 'body'
+ next()
+ },
+ ImageUploader.single('file'),
+ async (req, res) => {
+ const {
email: userEmail,
name,
phoneNumber,
- password: hashedPassword,
+ password,
gender,
height,
weight,
- file,
favoriteStyle,
- })
-
- await newUser.save()
-
- const port = process.env.PORT
- let webAPI
-
- if (process.env.NODE_ENV === 'development') {
- webAPI = `http://localhost:${port}`
- } else {
- webAPI = 'http://www.model-fit.kro.kr'
+ } = req.body
+ const file = req.file ? req.file.location : undefined
+ // 저장한 파일 경로에서 userID 추출
+ const userId = req.file.key.split('/')[0]
+
+ try {
+ const userExists = await User.findOne({ email: userEmail })
+
+ if (userExists) {
+ return res.status(400).json({ error: '이미 가입된 이메일입니다.' })
+ }
+
+ // 비밀번호 해시화
+ const hashedPassword = await bcrypt.hash(password, 10)
+
+ // 새로운 사용자 생성 및 저장
+ const newUser = new User({
+ _id: userId,
+ email: userEmail,
+ name,
+ phoneNumber,
+ password: hashedPassword,
+ gender,
+ height,
+ weight,
+ file,
+ favoriteStyle,
+ })
+
+ await newUser.save()
+
+ const port = process.env.PORT
+ let webAPI
+
+ if (process.env.NODE_ENV === 'development') {
+ webAPI = `http://localhost:${port}`
+ } else {
+ webAPI = process.env.WEB_API
+ }
+ console.log('before parse!')
+
+ // 사용자이미지 전처리 human parse
+ const aiApiParseResponse = await axios.post(
+ process.env.AI_PARSE_API,
+ null,
+ {
+ params: { ID: userId, image_url: file },
+ }
+ )
+
+ console.log('parse complete!')
+
+ if (aiApiParseResponse.data.error) {
+ console.error('Error from AI API:', aiApiParseResponse.data.error)
+ res.status(500).json({ success: false, error: 'AI API failed.' })
+ return
+ }
+
+ // 사이즈 받아오기
+ const responseFromAIApi = await axios.get(aiSizeApi, {
+ params: { height: height, weight: weight },
+ })
+
+ if (responseFromAIApi.data.error) {
+ console.error('Error from AI API:', responseFromAIApi.data.error)
+ res.status(500).json({ success: false, error: 'AI API failed.' })
+ return
+ }
+
+ const { length, shoulderWidth, chestWidth } = responseFromAIApi.data.size
+
+ // 사이즈 프로필 생성 및 저장
+ const newSizeProfile = new SizeProfile({
+ userId: newUser._id,
+ length,
+ shoulderWidth,
+ chestWidth,
+ })
+
+ await newSizeProfile.save()
+
+ // 사용자 정보 업데이트
+ await User.findByIdAndUpdate(newUser._id, {
+ sizeProfile: newSizeProfile._id,
+ })
+
+ // 자동로그인
+ const { _id, email } = newUser
+ const jwt_secret = process.env.JWT_SECRET
+ const token = jwt.sign({ _id, email }, jwt_secret, { expiresIn: '1h' })
+
+ return res.status(201).json({
+ success: true,
+ message: '가입 및 로그인 성공!',
+ user: { _id, email },
+ token: token,
+ })
+ } catch (error) {
+ console.error('Registration Error:', error)
+ res.status(500).json({ success: false, error: 'Registration failed.' })
}
-
- // 사이즈 받아오기
- const aiAPI = process.env.AI_API
- const aiApiEndpoint = `${aiAPI}/${AI_SIZE_ENDPOINT}`
- const responseFromAIApi = await axios.post(aiApiEndpoint, {
- imageUrl: file,
- height,
- weight,
- userId: newUser._id,
- callbackUrl: `${webAPI}/api/size`,
- })
-
- if (responseFromAIApi.data.error) {
- console.error('Error from AI API:', responseFromAIApi.data.error)
- res.status(500).json({ success: false, error: 'AI API failed.' })
- return
- }
-
- // 자동로그인
- const { _id, email } = newUser
- const jwt_secret = process.env.JWT_SECRET
- const token = jwt.sign({ _id, email }, jwt_secret, { expiresIn: '1h' })
-
- return res.status(201).json({
- success: true,
- message: '가입 및 로그인 성공!',
- user: { _id, email },
- token: token,
- })
- } catch (error) {
- console.error('Registration Error:', error)
- res.status(500).json({ success: false, error: 'Registration failed.' })
}
-})
+)
// 사이즈 정보 받아오기
router.post('/api/size', async (req, res) => {
diff --git a/src/routes/UploadClothImage.js b/src/routes/UploadClothImage.js
new file mode 100644
index 0000000..d068e55
--- /dev/null
+++ b/src/routes/UploadClothImage.js
@@ -0,0 +1,64 @@
+const express = require('express')
+const router = express.Router()
+const Closet = require('../models/Closet')
+const { ImageUploader } = require('./ImageUploader')
+
+router.post(
+ '/cloth-upload',
+ (req, res, next) => {
+ req.query.type = 'clothing'
+ ImageUploader.single('clothingImage')(req, res, (error) => {
+ if (error) {
+ return res.status(400).json({ success: false, error: error.message })
+ }
+ next()
+ })
+ },
+ async (req, res) => {
+ // 유효성 검사: 요청 userId 확인
+ if (!req.body.userId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Missing userId in request.',
+ })
+ }
+
+ const userId = req.body.userId
+ // 이미지 업로드 확인
+ if (!req.file || !req.file.location) {
+ return res.status(500).json({
+ success: false,
+ error: 'Image upload failed.',
+ })
+ }
+
+ // 이미지 URL을 생성
+ const clothingImageUrl = req.file.location
+
+ // 새 Closet 생성 및 저장
+ try {
+ const newCloset = new Closet({
+ userId: userId,
+ clothesUrl: req.body.clothesUrl,
+ clothesImageLink: clothingImageUrl,
+ })
+
+ const savedCloset = await newCloset.save()
+
+ // 성공적으로 저장된 경우 응답
+ res.status(200).json({
+ success: true,
+ message: 'Closet entry created successfully.',
+ savedCloset,
+ })
+ } catch (error) {
+ console.error('Error saving to Closet:', error)
+ res.status(500).json({
+ success: false,
+ error: 'Error saving to Closet.',
+ })
+ }
+ }
+)
+
+module.exports = router
diff --git a/src/routes/UserInfo.js b/src/routes/UserInfo.js
new file mode 100644
index 0000000..16cc819
--- /dev/null
+++ b/src/routes/UserInfo.js
@@ -0,0 +1,252 @@
+// 사이즈: backend -> frontend
+const express = require('express')
+const router = express.Router()
+
+const mongoose = require('mongoose')
+const { Schema } = mongoose
+
+const axios = require('axios')
+
+const User = require('../models/User')
+const SizeProfile = require('../models/SizeProfile')
+
+const sizeAPI = process.env.AI_SIZE_API_URL
+
+const { ImageUploader } = require('./ImageUploader')
+
+// 사이즈 : backend -> frontend
+router.get('/api/size', async (req, res) => {
+ console.log('get /userInfo/api/size')
+ const { userId } = req.query
+ try {
+ const sizeProfile = await SizeProfile.findOne({ userId: userId })
+ res.status(201).json({ success: true, message: sizeProfile })
+ } catch (error) {
+ console.error('Error finding size :', error)
+ res.status(500).json({ success: false, error: 'Failed to fetch size.' })
+ }
+})
+
+router.get('/api/info', async (req, res) => {
+ console.log('get /userInfo/api/info')
+
+ const { userId } = req.query
+
+ try {
+ const user = await User.findById({ _id: userId })
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: 'The user was not found.' })
+ }
+ res.status(201).json({ success: true, user })
+ } catch (error) {
+ console.error('User lookup errors :', error)
+ res.status(500).json({
+ success: false,
+ message: 'An error occurred during user lookup.',
+ })
+ }
+})
+
+// 사용자 정보 업데이트
+router.put('/api/privacy', async (req, res) => {
+ console.log('put /userInfo/api/privacy')
+ const user = req.body
+ const userId = user.userId
+
+ const update = {
+ $set: user,
+ }
+
+ try {
+ const result = await User.findByIdAndUpdate({ _id: userId }, update)
+
+ res.status(201).json({ success: true, code: 'UPDATE_DONE', errno: 0 })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ code: error.codeName,
+ errno: error.code,
+ /* message: 'An error occurred while updating the document.', */
+ })
+ }
+})
+
+// 사이즈 정보 업데이트
+router.put('/api/size', async (req, res) => {
+ console.log('put /userInfo/api/size')
+ const reqSize = req.body
+ const userId = reqSize.userId
+
+ let isShoulderWidthNull = reqSize.shoulderWidth ? false : true
+ let isChestWidthNull = reqSize.chestWidth ? false : true
+ let isLengthNull = reqSize.length ? false : true
+
+ let sizeData = {
+ shoulderWidth: reqSize.shoulderWidth,
+ chestWidth: reqSize.chestWidth,
+ length: reqSize.length,
+ }
+ let userData = { height: reqSize.height, weight: reqSize.weight }
+ let sizeResponse
+
+ // 하나라도 null인 경우 size api 사용해서 값 가져옴
+ if (isShoulderWidthNull || isChestWidthNull || isLengthNull) {
+ sizeResponse = await axios.get(sizeAPI, {
+ params: userData,
+ })
+ // null인 값 sizeRes로 채워줌
+ if (isShoulderWidthNull)
+ sizeData.shoulderWidth = sizeResponse.data.size.shoulderWidth
+ if (isChestWidthNull)
+ sizeData.chestWidth = sizeResponse.data.size.chestWidth
+ if (isLengthNull) sizeData.length = sizeResponse.data.size.length
+
+ if (sizeResponse.data.error) {
+ console.error('Error from AI API:', sizeResponse.data.error)
+ res
+ .status(500)
+ .json({ success: false, code: 'SIZE_API_FAILED', errno: -1 })
+ return
+ }
+ }
+
+ try {
+ const userUpdateRes = await User.findByIdAndUpdate(
+ { _id: userId },
+ {
+ $set: userData,
+ },
+ { new: true }
+ )
+ const sizeUpdateRes = await SizeProfile.findOneAndUpdate(
+ { userId: userId },
+ {
+ $set: sizeData,
+ },
+ { new: true }
+ )
+
+ res.status(201).json({ success: true, code: 'UPDATE_DONE', errno: 0 })
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ code: error.codeName,
+ errno: error.code,
+ })
+ }
+})
+
+// 사용자 신체 이미지 경로 전송
+router.get('/api/userimage', async (req, res) => {
+ const { userId } = req.query
+
+ try {
+ const user = await User.findById(userId)
+ if (!user) {
+ return res
+ .status(404)
+ .json({ success: false, message: 'The user was not found.' })
+ }
+ res.status(200).json({ success: true, image: user.file })
+ } catch (error) {
+ console.error('User lookup errors :', error)
+ res.status(500).json({
+ success: false,
+ message: 'An error occurred during user lookup.',
+ })
+ }
+})
+
+// kyi : 이거 뭐죠? 제가 만든 건가요? 흠..
+router.post(
+ '/api/userimage/change',
+ ImageUploader.single('image'),
+ async (req, res) => {
+ try {
+ if (req.file) {
+ res.status(200).json({ imageUrl: req.file.location })
+ } else {
+ res.status(400).send({ error: 'Image upload failed' })
+ }
+ } catch (error) {
+ console.error('Error processing image upload:', error)
+ res.status(500).send({ error: 'Error processing image upload' })
+ }
+ }
+)
+
+// router.post(
+router.put(
+ '/api/userimage',
+ (req, res, next) => {
+ console.log(req)
+ req.query.type = 'body'
+ next()
+ },
+ ImageUploader.single('file'),
+ async (req, res) => {
+ console.log('Step 2: Handling the response')
+ // const file = req.body.file
+ const file = req.file ? req.file.location : undefined
+ // 파라미터에서 이미지 ID 추출하고 없다면, 저장한 파일 경로에서 userID 추출
+ const userId =
+ (req.body.userId || req.query.userId) ?? req.file.key.split('/')[0]
+ console.log(file)
+ try {
+ // 업데이트할 정보
+ const update = {
+ $set: { file: file },
+ }
+ // 업데이트로 수정
+ const result = await User.findByIdAndUpdate({ _id: userId }, update)
+
+ const port = process.env.PORT
+ let webAPI
+
+ if (process.env.NODE_ENV === 'development') {
+ webAPI = `http://localhost:${port}`
+ } else {
+ webAPI = process.env.WEB_API
+ }
+
+ // 사용자이미지 전처리 human parse
+ const aiApiParseResponse = await axios.post(
+ process.env.AI_PARSE_API,
+ null,
+ {
+ params: { ID: userId, image_url: file },
+ }
+ )
+ if (aiApiParseResponse.data.error) {
+ console.error('Error from AI API:', aiApiParseResponse.data.error)
+ if (
+ aiApiParseResponse.data.message ===
+ 'human parse 실행 중 오류가 발생했습니다: cannot write mode RGBA as JPEG'
+ ) {
+ res
+ .status(500)
+ .json({ success: false, code: 'CAN_NOT_WRITE', errno: -1 })
+ return
+ }
+ res
+ .status(500)
+ .json({ success: false, code: 'AI_PARSE_ERROR', errno: -1 })
+ return
+ }
+
+ console.log('Parse done!')
+ res
+ .status(200)
+ .json({ success: true, code: 'IMAGE_CHANGE_DONE', errno: 0, url: file })
+ } catch (error) {
+ console.error('Profile Image CHange Error:', error)
+ res
+ .status(500)
+ .json({ success: false, code: 'IMAGE_CHANGE_FAIL', errno: -1 })
+ }
+ }
+)
+
+module.exports = router
diff --git a/src/routes/crawling.js b/src/routes/crawling.js
new file mode 100644
index 0000000..da28614
--- /dev/null
+++ b/src/routes/crawling.js
@@ -0,0 +1,111 @@
+const axios = require('axios')
+const cheerio = require('cheerio')
+
+const getHtml = async (url) => {
+ try {
+ const response = await axios.get(url)
+ return { success: true, code: 'FETCH_DONE', data: response.data }
+ } catch (error) {
+ return { success: false, code: error.code, errno: error.errno }
+ }
+}
+
+// 60.jpg -> 500.jpg로 바꾸기 위한 함수
+// 뒤부터 문자열 탐색 후 교체
+function replaceLastOccurrence(str, find, replace) {
+ const lastIndex = str.lastIndexOf(find)
+ if (lastIndex === -1) {
+ return str
+ }
+ const before = str.substring(0, lastIndex)
+ const after = str.substring(lastIndex + find.length)
+ return before + replace + after
+}
+
+// 무신사용 크롤러
+const extractImgSrcFromMusinsa = (html) => {
+ let imageSrcList = []
+ const $ = cheerio.load(html)
+
+ $('ul.product_thumb img').each((index, element) => {
+ const src = $(element).attr('src')
+ if (checkImageExtension(src)) {
+ const modifiedSrc = 'https:' + replaceLastOccurrence(src, '60', '500')
+ imageSrcList.push(modifiedSrc)
+ }
+ })
+
+ $('div.detail_product_info_item img').each((index, element) => {
+ const src = $(element).attr('src')
+ // 상품 상세보기 페이지의 이미지는 확장자 없는 경우도 있음...
+ // if (!checkImageExtension(src)) {
+ // return true
+ // }
+ if (!src.startsWith('//')) {
+ imageSrcList.push(src)
+ }
+ })
+
+ return imageSrcList
+}
+
+// 일반 웹사이트용 크롤러
+const extractImgSrc = (html) => {
+ let imageSrcList = []
+ const $ = cheerio.load(html)
+
+ $('img').each((index, element) => {
+ const src = $(element).attr('src')
+ // 이미지 확장자로 사진 걸러주는 부분
+ // => 확장자로 끝나지 않는 이미지도 꽤 많아 제거
+ // if (!checkImageExtension(src)) {
+ // return true
+ // }
+ if (src.startsWith('//')) {
+ imageSrcList.push('https:' + src)
+ } else if (!src.startsWith(':data')) {
+ imageSrcList.push(src)
+ }
+ })
+
+ return imageSrcList
+}
+
+// 파일 이름이 이미지 확장자인지 확인
+function checkImageExtension(url) {
+ // const imageExtensions = /\.(jpg|jpeg|png|gif|bmp)$/i
+ const imageExtensions = /\.(jpg|jpeg|png)$/i
+ return imageExtensions.test(url)
+}
+
+// 메인 크롤러
+const getClothesImageUrls = async (url) => {
+ // http로 시작하는지 체크
+ if (!url.startsWith('http')) {
+ return { success: false, code: 'URL_ERROR_HTTP', errno: -1 }
+ }
+
+ // 이미지의 url을 바로 올렸을 경우 체크
+ if (checkImageExtension(url)) {
+ const response = await axios.get(url)
+ if (response.status === 200) {
+ return { success: true, code: 'FETCH_DONE', urls: [url] }
+ } else {
+ return { success: false, code: 'URL_ERROR_INVALID_IMAGE', errno: -1 }
+ }
+ }
+ const result = await getHtml(url)
+ if (!result.success) {
+ return result
+ }
+ const html = result.data
+ if (url.includes('musinsa')) {
+ const imageSrcList = extractImgSrcFromMusinsa(html)
+ return { success: true, code: 'FETCH_DONE', urls: imageSrcList, errno: 0 }
+ } else {
+ const imageSrcList = extractImgSrc(html)
+ return { success: true, code: 'FETCH_DONE', urls: imageSrcList, errno: 0 }
+ }
+}
+
+module.exports = { getClothesImageUrls }
diff --git a/style/babel-plugin-macros.config.js b/style/babel-plugin-macros.config.js
new file mode 100644
index 0000000..76dc1ec
--- /dev/null
+++ b/style/babel-plugin-macros.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ 'fontawesome-svg-core': {
+ license: 'free',
+ },
+}
diff --git a/style/babelrc.config.js b/style/babelrc.config.js
new file mode 100644
index 0000000..1f893cd
--- /dev/null
+++ b/style/babelrc.config.js
@@ -0,0 +1,7 @@
+module.exports = function (api) {
+ // api.cache.forever()
+ api.cache(true)
+ return {
+ plugins: ['macros'],
+ }
+}
diff --git a/style/package.json b/style/package.json
index b27ab55..783646d 100644
--- a/style/package.json
+++ b/style/package.json
@@ -3,23 +3,32 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@fortawesome/fontawesome-free": "^6.4.0",
+ "@fortawesome/fontawesome-svg-core": "^6.4.0",
+ "@fortawesome/free-regular-svg-icons": "^6.4.0",
+ "@fortawesome/free-solid-svg-icons": "^6.4.0",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.4.0",
+ "babel-plugin-macros": "^3.1.0",
"bootstrap": "^5.3.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
+ "prop-types": "^15.8.1",
"react": "^18.2.0",
"react-bootstrap": "^2.7.4",
"react-dom": "^18.2.0",
"react-redux": "^8.1.1",
+ "react-responsive-carousel": "^3.2.23",
"react-router-dom": "^6.13.0",
- "react-scripts": "5.0.1",
+ "react-scripts": "^5.0.1",
"react-tooltip": "^5.16.1",
"redux": "^4.2.1",
"styled-components": "^6.0.0-rc.3",
+ "sweetalert2": "^11.7.12",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/style/src/App.js b/style/src/App.js
index 3e06774..9008a59 100644
--- a/style/src/App.js
+++ b/style/src/App.js
@@ -1,18 +1,34 @@
-import React from 'react'
+import React, { useEffect } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { login } from './features/authSlices'
import MainPage from './components/MainPage/MainPage'
-import FittingPage from './components/FittingPage.js'
-import RecommendPage from './components/RecommendPage.js'
+import FittingPage from './components/FittingPage/FittingPage.js'
+import RecommendPage from './components/RecommendPage/RecommendPage.js'
import PolicyTerms from './components/Terms/PolicyTerms.js'
import PrivacyMain from './components/Terms/PrivacyMain.js'
import 'react-bootstrap'
import { Routes, Route } from 'react-router-dom'
+import MyPage from './components/MyPage/MyPage.js'
function App() {
+ const dispatch = useDispatch()
+
+ const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
+ const lsUser = JSON.parse(localStorage.getItem('user'))
+ const lsToken = localStorage.getItem('token')
+
+ useEffect(() => {
+ if (!isAuthenticated && lsUser && lsToken) {
+ dispatch(login({ user: lsUser, token: lsToken }))
+ }
+ }, [dispatch])
+
return (
} />
} />
} />
+ } />
} />
} />
diff --git a/style/src/api/apiConfig.js b/style/src/api/apiConfig.js
index e74c1f2..230bb6c 100644
--- a/style/src/api/apiConfig.js
+++ b/style/src/api/apiConfig.js
@@ -1,3 +1,3 @@
export const API_URL = 'http://localhost:3000'
// 배포 시, 위 코드 주석처리 후 다음 코드로 변경
-// export const API_URL = 'http://www.model-fit.kro.kr'
+//export const API_URL = 'http://model-fit.kro.kr'
diff --git a/style/src/api/authenticatedAxios.js b/style/src/api/authenticatedAxios.js
index cce5bf3..d68457f 100644
--- a/style/src/api/authenticatedAxios.js
+++ b/style/src/api/authenticatedAxios.js
@@ -8,6 +8,11 @@ authenticatedAxios.interceptors.request.use(
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
+
+ if (config.data instanceof FormData) {
+ config.headers['Content-Type'] = 'multipart/form-data'
+ }
+
return config
},
(error) => {
diff --git a/style/src/components/FittingPage.js b/style/src/components/FittingPage.js
deleted file mode 100644
index 5483cfc..0000000
--- a/style/src/components/FittingPage.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Navigation from './Navigationbar/Nav'
-import Footer from './Footer'
-
-import React from 'react'
-import { useSelector } from 'react-redux'
-
-function FittingPage() {
- const user = useSelector((state) => state.auth.user)
-
- return (
-
-
- {user && (
-
-
환영합니다, {user.email}님!
-
- )}
-
-
- )
-}
-export default FittingPage
diff --git a/style/src/components/FittingPage/ButtonBar.js b/style/src/components/FittingPage/ButtonBar.js
new file mode 100644
index 0000000..f7173df
--- /dev/null
+++ b/style/src/components/FittingPage/ButtonBar.js
@@ -0,0 +1,306 @@
+import React, { useRef, useState } from 'react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { solid, regular } from '@fortawesome/fontawesome-svg-core/import.macro'
+
+import CardButton from './CardButton'
+import authenticatedAxios from '../../api/authenticatedAxios'
+import styles from './FittingPage.module.css'
+
+import saveImage from '../../images/save.png'
+import uploadImage from '../../images/upload.png'
+import changeBgImage from '../../images/landscape.png'
+import resetImage from '../../images/reset.png'
+import helpImage from '../../images/help.png'
+import { API_URL } from '../../api/apiConfig'
+import ClassMerger from '../common/ClassNameGenerater'
+import LoadingIndicator from '../LoadingIndicator'
+
+import { Carousel } from 'react-responsive-carousel'
+import 'react-responsive-carousel/lib/styles/carousel.min.css'
+import fithelp1 from '../../images/fithelp1.png'
+import fithelp2 from '../../images/fithelp2.png'
+import fithelp3 from '../../images/fithelp3.png'
+import fithelp4 from '../../images/fithelp4.png'
+
+function ButtonBar({
+ setErrorCode,
+ showAlert,
+ setIsDefaultPage,
+ image,
+ changeBackground,
+ setClosetUpdateTrigger,
+}) {
+ // URL 입력 저장을 위한 state 생성
+ const [inputUrl, setInputUrl] = useState('')
+ const [isModalOpen, setIsModalOpen] = useState(false)
+ const [clothes, setClothes] = useState([])
+ const [selected, setSelected] = useState(-1)
+ const [isLoading, setIsLoading] = useState(false)
+
+ // 도움말 팝업
+ const [isHelpModalOpen, setIsHelpModalOpen] = useState(false)
+ const handleHelpModalOpen = () => {
+ setIsHelpModalOpen(true)
+ }
+ const handleHelpModalClose = () => {
+ setIsHelpModalOpen(false)
+ }
+
+ // 모달창 밖 클릭 시 모달 창 종료에 사용
+ const uploadmodelBg = useRef()
+ const mouseDownEvent = useRef()
+ const mouseUpEvent = useRef()
+ const closeModalBtn = useRef()
+
+ const handleInputUrl = (e) => {
+ setInputUrl(e.target.value)
+ }
+
+ // URL 이미지 업로드 버튼 클릭 이벤트 핸들러
+ const handleImgUploadClick = async () => {
+ if (selected < 0 || selected >= clothes.length) {
+ // 이미지 선택이 없거나 범위를 벗어난 경우
+ return
+ }
+
+ setIsLoading(true)
+
+ const file = await fetch(clothes[selected])
+ .then((response) => response.blob())
+ .then(
+ (blob) =>
+ new File([blob], `clothes-${Date.now()}.png`, { type: 'image/png' })
+ )
+
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (!user || !user._id) {
+ console.error('User ID not found')
+ return
+ }
+ const userId = user._id
+ const urlWithoutQueryString = inputUrl.split('?')[0]
+
+ const formData = new FormData()
+ formData.append('userId', userId)
+ formData.append('clothingImage', file)
+ formData.append('clothesUrl', urlWithoutQueryString)
+ for (let value of formData.values()) {
+ console.log(value)
+ }
+ try {
+ const response = await authenticatedAxios.post(
+ `${API_URL}/api/cloth-upload`,
+ formData
+ )
+ if (response.status === 200) {
+ console.log('업로드에 성공했습니다!', response.data)
+ setIsModalOpen(false)
+ // 새로고침
+ // window.location.reload()
+ setClosetUpdateTrigger((current) => current + 1)
+ } else {
+ console.error('이미지 업로드에 실패했습니다.', response)
+ }
+ } catch (error) {
+ console.error('이미지 업로드 중 오류가 발생했습니다.', error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ // url 업로드 버튼 : url에서 이미지 목록 가져옴
+ const handleUrlUploadClick = async (event) => {
+ try {
+ const response = await authenticatedAxios.get(
+ `${API_URL}/cloth/api/clothesFromUrl`,
+ { params: { url: inputUrl } }
+ )
+
+ setErrorCode(response.data.code)
+ showAlert()
+ if (response.status === 200) {
+ setClothes(response.data.urls)
+ }
+ } catch (error) {
+ console.log(error.response.data.code)
+ setErrorCode(error.response.data.code)
+ showAlert()
+ }
+ }
+
+ const handleModalOpen = () => {
+ setIsModalOpen(true)
+ }
+
+ const handleModalClose = (event) => {
+ // close 버튼 누른 경우 창 닫기
+ if (
+ mouseDownEvent.current === closeModalBtn.current &&
+ mouseUpEvent.current === closeModalBtn.current
+ ) {
+ setIsModalOpen(false)
+ }
+ // 모달창 바깥 클릭 시 창 닫기
+ // 바깥 -(드래그)-> 안, 안 -(드래그)-> 밖도 꺼지지 않게 함
+ // 위 경우도 꺼지게 하는 조건 (event.current === uploadmodelBg.current)
+ else if (
+ mouseDownEvent.current === uploadmodelBg.current &&
+ mouseUpEvent.current === uploadmodelBg.current
+ ) {
+ setIsModalOpen(false)
+ }
+ }
+
+ // 불러온 이미지 하나 선택
+ const clothesImageSelect = (index) => {
+ setSelected(index)
+ }
+
+ // 이미지 저장
+
+ return (
+
+
+
+ {}}
+ />
+
+
+
setIsDefaultPage(true)}
+ />
+
+
+ {isHelpModalOpen && (
+
+
event.stopPropagation()}
+ >
+
도움말
+
+
+

+
+
+

+
+
+

+
+
+

+
+
+
+
+
+ )}
+
+ {isModalOpen && (
+ {
+ mouseDownEvent.current = event.target
+ }}
+ onMouseUp={(event) => {
+ mouseUpEvent.current = event.target
+ }}
+ ref={uploadmodelBg}
+ >
+
event.stopPropagation()}
+ >
+
의류 올리기
+
ㅤ잘 나온 사진 하나를 선택해주세요!
+
+
+
+ {clothes &&
+ clothes.map((src, index) => (
+
+ ))}
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+ )}
+
+ )
+}
+
+function ClothesImageElement({ src, index, selected, onClick }) {
+ return (
+ onClick(index)}
+ >
+
+

+
+ )
+}
+
+export default ButtonBar
diff --git a/style/src/components/FittingPage/CardButton.js b/style/src/components/FittingPage/CardButton.js
new file mode 100644
index 0000000..094841f
--- /dev/null
+++ b/style/src/components/FittingPage/CardButton.js
@@ -0,0 +1,14 @@
+import React from 'react'
+
+import styles from './FittingPage.module.css'
+
+function CardButton({ src, alt, text, onClick }) {
+ return (
+
+ )
+}
+
+export default CardButton
diff --git a/style/src/components/FittingPage/FittingPage.js b/style/src/components/FittingPage/FittingPage.js
new file mode 100644
index 0000000..8a638cd
--- /dev/null
+++ b/style/src/components/FittingPage/FittingPage.js
@@ -0,0 +1,90 @@
+import Navigation from '../Navigationbar/Nav'
+
+import LeftFitContainer from './LeftFitContainer'
+import RightFitContainer from './RightFitContainer'
+
+import styles from './FittingPage.module.css'
+
+import React from 'react'
+import { useState } from 'react'
+
+import bg1 from '../../images/bg1.png'
+import bg2 from '../../images/bg2.jpg'
+import bg3 from '../../images/bg3.jpg'
+import bg4 from '../../images/bg4.jpg'
+
+import { Navigate } from 'react-router-dom'
+import FittingPageAlert from './FittingPageAlert'
+import 'react-responsive-carousel/lib/styles/carousel.min.css'
+
+function FittingPage() {
+ // const user = useSelector((state) => state.auth.user)
+ const [fittingImage, setFittingImage] = useState('')
+ const [isDefaultPage, setIsDefaultPage] = useState(true)
+ const [isShowAlert, setIsShowAlert] = useState(false)
+ const [errorCode, setErrorCode] = useState(null)
+ const [bgIndex, setBgIndex] = useState(0)
+
+ // 이미지 업로드 후 옷장 reload에 사용하는 변수
+ const [closetUpdateTrigger, setClosetUpdateTrigger] = useState(0)
+
+ const backgroundList = [bg1, bg2, bg3, bg4]
+
+ const lsUser = JSON.parse(localStorage.getItem('user'))
+ const lsToken = localStorage.getItem('token')
+
+ const showAlert = (time = 3000) => {
+ setIsShowAlert(true)
+ setTimeout(() => {
+ setIsShowAlert(false)
+ }, time)
+ }
+
+ const changeBackground = () =>
+ setBgIndex((current) => (current + 1) % backgroundList.length)
+
+ if (lsUser && lsToken) {
+ return (
+ <>
+
+
+
+ >
+ )
+ } else {
+ return
+ }
+}
+export default FittingPage
diff --git a/style/src/components/FittingPage/FittingPage.module.css b/style/src/components/FittingPage/FittingPage.module.css
new file mode 100644
index 0000000..9d11b3e
--- /dev/null
+++ b/style/src/components/FittingPage/FittingPage.module.css
@@ -0,0 +1,487 @@
+@font-face {
+ font-family: 'BMJUA';
+ src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_one@1.0/BMJUA.woff') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@import url('https://cdn.rawgit.com/moonspam/NanumSquare/master/nanumsquare.css');
+
+.mainContainer {
+ --nav-height: 95px;
+ --max-width: 1920px;
+ --max-height: 970px;
+ --leftContiner-width: 466px;
+ --rightContiner-width: calc(var(--max-width) - var(--leftContiner-width));
+ /* --height : calc(100vh - 56px); */
+ --height: calc(var(--max-height) - var(--nav-height));
+
+
+ display: grid;
+ grid-template-columns: 4% 46% 4% 42% 4%;
+ height: var(--height);
+ /* overflow: hidden; */
+}
+
+.cardButton {
+ display: grid;
+ place-items: center;
+ text-align: center;
+ width: 140px;
+ height: 100px;
+ border-radius: 12px;
+ border-width: 0px;
+ background-color: #fbf7f2;
+ margin: 24px;
+}
+
+.cardButton:hover {
+ background-color: #f1ede9;
+}
+
+.cardButton .btnImage {
+ width: 60px;
+ height: 60px;
+}
+
+.cardButton .btnText {
+ font-family: 'NanumSquare';
+ color: #909090;
+ font-weight: 600;
+}
+
+.buttonContainer {
+ display: flex;
+ justify-content: space-between;
+}
+
+.imageContainer {
+ place-items: center;
+ text-align: center;
+ margin-top: 5%;
+}
+
+.fittingImage {
+ height: 650px;
+ object-fit: cover;
+}
+
+.rightFitContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.closetContainer {
+ display: inline-block;
+ background-color: #eae7e1;
+ width: 775px;
+ height: 805px;
+ border: 1px solid black;
+ border-radius: 4%;
+ overflow-y: hidden;
+}
+
+.closetNavbar {
+ height: 26%;
+ width: 100%;
+ background-color: grey;
+}
+
+.searchBarContainer {
+ background-color: #f0f0f0;
+ width: 100%;
+ height: 30%;
+ display: flex;
+ justify-content: center;
+ padding-top: 2.2%;
+}
+
+.searchBar {
+ display: inline-block;
+ width: 92%;
+ height: 40px;
+ border: 1.6px solid #b0b0b0;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+}
+
+.searchImage {
+ margin-left: 1.8%;
+}
+
+.searchInput {
+ width: 91%;
+ margin-left: 1%;
+ background: transparent;
+ border: 0px;
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.searchInput::placeholder {
+ color: #aaaaaa;
+}
+
+.searchInput:focus {
+ outline: none;
+}
+
+.mainMenuContainer {
+ background-color: #f0f0f0;
+ width: 100%;
+ height: 24%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.mainMenu {
+ display: inline-block;
+ width: 92%;
+ /* border: 1px solid black; */
+}
+
+.mainMenuBtn {
+ height: 30px;
+ width: 80px;
+ border-radius: 15px;
+ border: 0px;
+
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.mainMenuBtn:hover {
+ color: #ff7e36;
+ font-size: 18px;
+ font-weight: 600;
+ transition: all 0.3s ease-out;
+}
+
+.mainMenuBtn_selected {
+ height: 30px;
+ width: 80px;
+ border-radius: 15px;
+ border: 0px;
+ background-color: #323232;
+ color: #ff7e36;
+
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+ padding-top: 2px;
+}
+
+.menuControlBtn {
+ background: transparent;
+ border: 0px;
+ font-size: 20px;
+ color: #505d6f;
+ margin: 0 1.5%;
+}
+
+.subMenu1Container {
+ background-color: #eae7e1;
+ width: 100%;
+ height: 23%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.subMenu1 {
+ display: inline-block;
+ width: 92%;
+}
+
+.subMenu1Btn,
+.subMenu1Btn_selected {
+ background: transparent;
+ border: 0px;
+ width: 72px;
+ position: relative;
+
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.subMenu1Btn::after {
+ content: '';
+ display: block;
+ position: absolute;
+ box-sizing: border-box;
+ bottom: -12px;
+ left: 16px;
+ width: 60%;
+ height: 5px;
+ transition: all 0.3s ease-out;
+}
+
+.subMenu1Btn:hover::after,
+.subMenu1Btn_selected::after {
+ content: '';
+ display: block;
+ position: absolute;
+ box-sizing: border-box;
+ bottom: -10px;
+ left: 15px;
+ width: 60%;
+ height: 5px;
+ background-color: #ff7e36;
+}
+
+.subMenu2Container {
+ background-color: #f0f0f0;
+ width: 100%;
+ height: 23%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.subMenu2Btn {
+ background: transparent;
+ border: 0px;
+ width: 100px;
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.subMenu2Btn_selected {
+ background: transparent;
+ border: 0px;
+ width: 100px;
+ color: #ff7e36;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.subMenu2Btn:hover {
+ color: #ff7e36;
+ font-size: 18px;
+ font-weight: 600;
+ transition: all 0.3s ease-out;
+}
+
+.filterBtn {
+ background: transparent;
+ border: 0px;
+ font-size: 18px;
+ margin-right: 1.6%;
+}
+
+.closet {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(220px, 1fr));
+ grid-auto-rows: 269px;
+ /* grid-gap: 20px; */
+ overflow-y: auto;
+ max-height: 72%;
+ justify-content: space-between;
+ padding: 5px;
+}
+
+.clothesElement {
+ width: 232px;
+ height: 254px;
+ display: inline-block;
+ border-radius: 6px;
+ background-color: #f6f6f6;
+ margin: 10px 10px;
+ padding: 12px;
+ /* border: 1px solid black; */
+}
+
+.clothesInner {
+ /* display: flex; */
+ display: block;
+ text-align: center;
+}
+
+.clothesImage {
+ display: block;
+ margin: 0 auto;
+ width: 160px;
+ height: 180px;
+}
+
+.favoriteBtn {
+ background: transparent;
+ border: 0px;
+
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.wearingBtn {
+ background: transparent;
+ border: 0px;
+
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.sizeBtn {
+ background: transparent;
+ border: 0px;
+
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.removeBtn {
+ background: transparent;
+ border: 0px;
+
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.modalContent {
+ background-color: white;
+ padding: 24px;
+ width: 100%;
+ max-width: 800px;
+ height: 800px;
+ border-radius: 4px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ position: relative;
+}
+
+.modalContent h2 {
+ font-family: 'NanumSquare';
+ font-weight: 600;
+ display: inline-block;
+}
+
+.modalContent span {
+ font-family: 'NanumSquare';
+ font-weight: 600;
+ display: inline-block;
+}
+
+.modalContent input {
+ width: 100%;
+ padding: 8px;
+ display: block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ margin-bottom: 16px;
+
+ color: #aaaaaa;
+ font-family: 'NanumSquare';
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.clothesImageSearchBtn {
+ background: transparent;
+ border: 0px;
+ position: absolute;
+ padding: 0;
+ top: 73px;
+ right: 40px;
+ font-size: 22px;
+}
+
+.modalImageContainer {
+ width: 100%;
+ height: 80%;
+ border: 1px solid #cccccc;
+ border-radius: 4px;
+ overflow-y: auto;
+ grid-template-columns: repeat(5, 148px);
+ grid-auto-rows: 152px;
+ display: grid;
+ justify-content: space-evenly;
+}
+
+.clothesImageBox {
+ position: relative;
+ display: inline-block;
+ border: 1px solid #cccccc;
+ border-radius: 4px;
+ margin: 1% 2%;
+}
+
+.clothesImageBox img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.clothesChecked,
+.clothesImageBox:hover {
+ filter: brightness(0.8);
+}
+
+.imageCheckBtn {
+ position: absolute;
+ background: transparent;
+ top: 1%;
+ left: 3%;
+ border: 0px;
+ padding: 0px;
+}
+
+.modalButtonContainer {
+ width: 100%;
+ height: 40px;
+ text-align: right;
+ margin-top: 12px;
+}
+
+.modalContent .modalButtonContainer button {
+ background-color: #007bff;
+ border: none;
+ color: white;
+ padding: 8px 16px;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 14px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-right: 8px;
+}
+
+.modalContent button:last-child {
+ background-color: #ccc;
+ }
+
+ .mainImage {
+ max-width: 100%;
+ height: auto;
+ }
+
\ No newline at end of file
diff --git a/style/src/components/FittingPage/FittingPageAlert.js b/style/src/components/FittingPage/FittingPageAlert.js
new file mode 100644
index 0000000..bf4c98a
--- /dev/null
+++ b/style/src/components/FittingPage/FittingPageAlert.js
@@ -0,0 +1,50 @@
+import AlertMessage from '../common/AlertMessage'
+
+function FittingPageAlert({ errorCode, isShowAlert, setIsShowAlert }) {
+ return (
+ <>
+ {errorCode === 'FETCH_DONE' && (
+
+ )}
+ {errorCode === 'URL_ERROR_HTTP' && (
+
+ )}
+ {errorCode === 'URL_ERROR_INVALID_IMAGE' && (
+
+ )}
+ {errorCode === 'ERR_BAD_REQUEST' && (
+
+ )}
+ {errorCode === 'DELETE_DONE' && (
+
+ )}
+ >
+ )
+}
+
+export default FittingPageAlert
diff --git a/style/src/components/FittingPage/LeftFitContainer.js b/style/src/components/FittingPage/LeftFitContainer.js
new file mode 100644
index 0000000..f34d1f2
--- /dev/null
+++ b/style/src/components/FittingPage/LeftFitContainer.js
@@ -0,0 +1,44 @@
+import React, { useEffect, useState } from 'react'
+
+import ButtonBar from './ButtonBar'
+
+import styles from './FittingPage.module.css'
+import 'react-responsive-carousel/lib/styles/carousel.min.css'
+import getProfileImage from '../common/getProfileImage'
+
+function LeftFitContainer({
+ setErrorCode,
+ showAlert,
+ fittingImage,
+ isDefaultPage,
+ setIsDefaultPage,
+ changeBackground,
+ setClosetUpdateTrigger,
+}) {
+ const [profileImage, setProfileImage] = useState('')
+
+ useEffect(() => {
+ getProfileImage(setProfileImage)
+ }, [])
+
+ return (
+
+
+
+

+
+
+ )
+}
+export default LeftFitContainer
diff --git a/style/src/components/FittingPage/RightFitContainer.js b/style/src/components/FittingPage/RightFitContainer.js
new file mode 100644
index 0000000..11a6dcd
--- /dev/null
+++ b/style/src/components/FittingPage/RightFitContainer.js
@@ -0,0 +1,393 @@
+import React, { memo } from 'react'
+import { useEffect, useState } from 'react'
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { solid, regular } from '@fortawesome/fontawesome-svg-core/import.macro'
+
+import styles from './FittingPage.module.css'
+import { API_URL } from '../../api/apiConfig'
+
+import searchImage from '../../images/search.png'
+import axios from 'axios'
+
+import LoadingIndicator from '../LoadingIndicator'
+import Swal from 'sweetalert2'
+
+function RightFitContainer({
+ setFittingImage,
+ setIsDefaultPage,
+ setErrorCode,
+ showAlert,
+ closetUpdateTrigger,
+}) {
+ const [mainMenu, setMainMenu] = useState('closet')
+ const [subMenu1, setSubMenu1] = useState('all')
+ const [subMenu2, setSubMenu2] = useState('all')
+ const [clothes, setClothes] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const list_subMenu1 = ['all', 'top', 'bottom']
+
+ //서버에서 clothes 이미지 데이터를 가져오는 함수
+ const fetchClothesImages = async () => {
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (!user || !user._id) {
+ console.error('User ID not found')
+ return
+ }
+ const userId = user._id
+ try {
+ const response = await axios.get(`${API_URL}/cloth/api/clothes`, {
+ params: {
+ userId: userId,
+ },
+ })
+ console.log(response)
+ setClothes(
+ response.data.map((imageItem) => ({
+ id: imageItem._id,
+ src: imageItem.clothesImageLink,
+ }))
+ )
+ } catch (err) {
+ console.error('Error fetching clothes images:', err)
+ }
+ }
+ useEffect(() => {
+ fetchClothesImages()
+ }, [closetUpdateTrigger])
+
+ const toggleFavorite = (indexToToggle) => {
+ setClothes(
+ clothes.map((item, index) =>
+ index === indexToToggle ? { ...item, favorite: !item.favorite } : item
+ )
+ )
+ }
+
+ //피팅
+ const handleFittingCloth = async (clothID) => {
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (!user || !user._id) {
+ console.error('User ID not found')
+ return
+ }
+ const userId = user._id
+ try {
+ setIsLoading(true)
+ const response = await fetch('/api/fitting', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ userId, clothID }),
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ console.log(data.fittingImageLink)
+
+ setFittingImage(data.fittingImageLink)
+ setIsDefaultPage(false)
+ setIsLoading(false)
+ } else {
+ console.error('Error loading fitting image')
+ }
+ } catch (error) {
+ console.error('Fitting Request Error:', error)
+ setIsLoading(false)
+ }
+ }
+
+ //옷 제거
+ const handleDeleteCloth = async (clothId) => {
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (!user || !user._id) {
+ console.error('User ID not found')
+ return
+ }
+
+ const confirmation = window.confirm('옷을 삭제하시겠습니까?')
+ if (!confirmation) {
+ return
+ }
+ console.log(clothId)
+ try {
+ const response = await axios.delete(
+ `${API_URL}/cloth/api/delete/${clothId}`
+ )
+ fetchClothesImages()
+ setErrorCode(response.data.code)
+ showAlert()
+ } catch (error) {
+ console.error('Error deleting cloth:', error)
+ }
+ }
+
+ //사이즈 추천
+ const handleRecommendSize = async (clothId) => {
+ const user = JSON.parse(localStorage.getItem('user'))
+ if (!user || !user._id) {
+ console.error('User ID not found')
+ return
+ }
+ const userId = user._id
+
+ try {
+ const response = await axios.post(`${API_URL}/cloth/cloth_size`, {
+ userId,
+ clothId,
+ })
+
+ if (response.status === 200) {
+ const recommendedSize = response.data.size
+ Swal.fire({
+ icon: 'success',
+ title: '추천 사이즈',
+ text: `추천 사이즈는 ${recommendedSize} 입니다.`,
+ })
+ } else {
+ Swal.fire({
+ icon: 'error',
+ title: '오류 발생',
+ text: `Error: ${response.statusText}`,
+ })
+ }
+ } catch (error) {
+ Swal.fire({
+ icon: 'error',
+ title: '사이즈 추천 오류',
+ text: `Error: ${error.response.data.message}`,
+ })
+ console.error('Size Recommendation Error:', error)
+ }
+ }
+
+ return (
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading && }
+ {clothes.map((item, index) => (
+
+ ))}
+
+
+
+ )
+}
+export default RightFitContainer
+
+function ClothesElement({
+ id,
+ index,
+ src,
+ favorite,
+ onHeartClick,
+ consolea,
+ handleFittingCloth,
+ handleDeleteCloth,
+ handleRecommendSize,
+}) {
+ return (
+
+
+

+
+
+
+
+
+
+
+ )
+}
+
+const MemoClothesElement = memo(ClothesElement)
diff --git a/style/src/components/Footer.js b/style/src/components/Footer.js
index b349480..d1dea65 100644
--- a/style/src/components/Footer.js
+++ b/style/src/components/Footer.js
@@ -12,8 +12,8 @@ function Footer() {
- 이용 약관
- 개인정보처리방침
+ 이용 약관
+ 개인정보처리방침
diff --git a/style/src/components/LoadingIndicator.css b/style/src/components/LoadingIndicator.css
new file mode 100644
index 0000000..5af176f
--- /dev/null
+++ b/style/src/components/LoadingIndicator.css
@@ -0,0 +1,50 @@
+.loading-indicator-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ background-color: rgba(255,255,255,0.8);
+}
+
+.loading-indicator {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ width: 70px;
+ height: 35px;
+}
+
+.circle {
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ background-color: #333;
+ animation: bounce 2s infinite ease-in-out;
+}
+
+.circle:nth-child(1) {
+ animation-delay: .2s;
+}
+
+.circle:nth-child(2) {
+ animation-delay: .4s;
+}
+
+.circle:nth-child(3) {
+ animation-delay: .6s;
+}
+
+@keyframes bounce {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-15px);
+ }
+}
+
diff --git a/style/src/components/LoadingIndicator.js b/style/src/components/LoadingIndicator.js
new file mode 100644
index 0000000..d75db05
--- /dev/null
+++ b/style/src/components/LoadingIndicator.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import './LoadingIndicator.css'
+
+const LoadingIndicator = () => {
+ return (
+
+ )
+}
+
+export default LoadingIndicator
diff --git a/style/src/components/MyPage/FormBox.js b/style/src/components/MyPage/FormBox.js
new file mode 100644
index 0000000..ce86df3
--- /dev/null
+++ b/style/src/components/MyPage/FormBox.js
@@ -0,0 +1,58 @@
+import ClassMerger from '../common/ClassNameGenerater'
+// import Proptypes from 'prop-types'
+import { Form } from 'react-bootstrap'
+import styles from './MyPage.module.css'
+
+function FormBox({
+ id,
+ label,
+ type,
+ min,
+ max,
+ pattern,
+ setState,
+ value,
+ isInvalid,
+ invalidTest,
+ disabled = false,
+}) {
+ return (
+
+
+ {label}
+
+ {
+ const input = event.target.value
+ setState(input)
+ }}
+ />
+
+ {invalidTest}
+
+
+ )
+}
+
+// FormBox.propTypes = {
+// id: Proptypes.string.isRequired,
+// label: Proptypes.string.isRequired,
+// type: Proptypes.string.isRequired,
+// min: Proptypes.string,
+// max: Proptypes.string,
+// pattern: Proptypes.string,
+// setState: Proptypes.func,
+// disabled: Proptypes.bool,
+// }
+
+export default FormBox
diff --git a/style/src/components/MyPage/ImageUploader.css b/style/src/components/MyPage/ImageUploader.css
new file mode 100644
index 0000000..5a98966
--- /dev/null
+++ b/style/src/components/MyPage/ImageUploader.css
@@ -0,0 +1,67 @@
+.ImageUploader {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.image-container {
+ /* position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 300px;
+ height: 300px;
+ border: 1px dashed #ccc;
+ cursor: pointer; */
+ background-color: #dbdbdb;
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ justify-content: center;
+ align-items: center;
+ border: none;
+ font-size: 20px;
+ color: #434343;
+ position: absolute;
+ bottom: 36px;
+ left: 106px;
+}
+
+.preview-image {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.placeholder-text {
+ text-align: center;
+ position: relative;
+}
+
+.has-image {
+ border: none;
+}
+
+.remove-button {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 20px;
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 14px;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
+}
+
+.remove-button:hover {
+ background-color: rgba(255, 0, 0, 0.7);
+ box-shadow: 0 0 6px rgba(255, 0, 0, 0.4);
+ transition: all 0.3s ease;
+}
diff --git a/style/src/components/MyPage/ImageUploader.js b/style/src/components/MyPage/ImageUploader.js
new file mode 100644
index 0000000..d9f1fcf
--- /dev/null
+++ b/style/src/components/MyPage/ImageUploader.js
@@ -0,0 +1,70 @@
+import { useState, useRef } from 'react'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { solid, regular } from '@fortawesome/fontawesome-svg-core/import.macro'
+import './ImageUploader.css'
+
+//Single Image Upload
+
+function ImageUploader({ onChange }) {
+ const [uploadedImage, setUploadedImage] = useState(null)
+ const inputRef = useRef()
+ const allowedExtensions = ['png', 'jpg', 'jpeg', 'bmp']
+
+ const handleFileChange = (e) => {
+ const file = e.target.files[0]
+ if (file && file.type.startsWith('image/')) {
+ const extension = file.name.split('.').pop()?.toLowerCase()
+ if (!allowedExtensions.includes(extension)) {
+ alert('Invalid file extension. Only images are allowed.')
+ return
+ }
+
+ const reader = new FileReader()
+ reader.onloadend = (e) => {
+ const result = e.target.result
+ if (result) {
+ setUploadedImage(result)
+ // onChange(file)
+ }
+ }
+ reader.readAsDataURL(file)
+
+ if (typeof onChange === 'function') {
+ onChange(file)
+ }
+ }
+ }
+
+ const handleClick = () => {
+ inputRef.current.click()
+ }
+
+ const handleDragOver = (e) => {
+ e.preventDefault()
+ }
+
+ return (
+
+
+
+ {/* */}
+
+
+
+
+ )
+}
+
+export default ImageUploader
diff --git a/style/src/components/MyPage/LeftSubMyPage.js b/style/src/components/MyPage/LeftSubMyPage.js
new file mode 100644
index 0000000..8821ea8
--- /dev/null
+++ b/style/src/components/MyPage/LeftSubMyPage.js
@@ -0,0 +1,115 @@
+import styles from './MyPage.module.css'
+import profileImage from '../../images/profileImageLoading.png'
+// import profileImage from '../../images/profile.jpg'
+import Proptypes from 'prop-types'
+import ClassMerger from '../common/ClassNameGenerater'
+import ImageUploader from './ImageUploader'
+import { API_URL } from '../../api/apiConfig'
+import authenticatedAxios from '../../api/authenticatedAxios'
+import { useEffect, useRef, useState } from 'react'
+import { useSelector } from 'react-redux'
+import getProfileImage from '../common/getProfileImage'
+
+// 선택된 버튼에 주황색 넣어주는 함수
+function TransButton({ context, option, page, onClick }) {
+ return (
+
+ )
+}
+
+TransButton.propTypes = {
+ context: Proptypes.string.isRequired,
+ option: Proptypes.string.isRequired,
+ onClick: Proptypes.func.isRequired,
+}
+
+function LeftSubMyPage({ page, onClickHandler }) {
+ const [profileUrl, setProfileUrl] = useState(profileImage)
+ const uploadFile = useRef(null)
+ const user = useSelector((state) => state.auth.user)
+
+ useEffect(() => {
+ getProfileImage(setProfileUrl)
+ }, [])
+
+ // 이미지 선택
+ const handleImageChange = async (file) => {
+ uploadFile.current = file
+ if (!uploadFile.current) return
+
+ const formData = new FormData()
+ formData.append('userId', user._id)
+ formData.append('file', uploadFile.current)
+
+ try {
+ const response = await authenticatedAxios.put(
+ `${API_URL}/userinfo/api/userimage`,
+ formData,
+ {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ }
+ )
+
+ if (response.status >= 200) {
+ setProfileUrl(`${response.data.url}?${Date.now()}`)
+ } else {
+ console.log('Profile Image Change failed.')
+ }
+ } catch (error) {
+ console.error('Error fetching images', error)
+ }
+ }
+
+ return (
+
+
마이페이지
+
+

+
+
+
+
+
+
+ {/*
+
*/}
+
+ )
+}
+LeftSubMyPage.propTypes = {
+ page: Proptypes.string.isRequired,
+ onClickHandler: Proptypes.func.isRequired,
+}
+
+export default LeftSubMyPage
diff --git a/style/src/components/MyPage/MyPage.js b/style/src/components/MyPage/MyPage.js
new file mode 100644
index 0000000..3d8bae6
--- /dev/null
+++ b/style/src/components/MyPage/MyPage.js
@@ -0,0 +1,52 @@
+import { useState } from 'react'
+import styles from './MyPage.module.css'
+import LeftSubMyPage from './LeftSubMyPage'
+import RightSubMyPage from './RightSubMyPage'
+import Navigation from '../Navigationbar/Nav.js'
+import { Navigate } from 'react-router-dom'
+import MyPageAlert from './MyPageAlert'
+
+function MyPage({ currPage = 'privacy' }) {
+ const [page, setPage] = useState(currPage)
+ const [isShowAlert, setIsShowAlert] = useState(false)
+ const [errorCode, setErrorCode] = useState(false)
+
+ const showAlert = (time = 3000) => {
+ setIsShowAlert(true)
+ setTimeout(() => {
+ setIsShowAlert(false)
+ }, time)
+ }
+
+ const changePage = (pageName) => {
+ setPage(pageName)
+ }
+
+ const lsUser = JSON.parse(localStorage.getItem('user'))
+ const lsToken = localStorage.getItem('token')
+
+ if (lsUser && lsToken) {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+ } else {
+ return
+ }
+}
+
+export default MyPage
diff --git a/style/src/components/MyPage/MyPage.module.css b/style/src/components/MyPage/MyPage.module.css
new file mode 100644
index 0000000..0dc6a53
--- /dev/null
+++ b/style/src/components/MyPage/MyPage.module.css
@@ -0,0 +1,205 @@
+@font-face {
+ font-family: 'BMJUA';
+ src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_one@1.0/BMJUA.woff') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@import url('https://cdn.rawgit.com/moonspam/NanumSquare/master/nanumsquare.css');
+
+.myPageRoot {
+ position: relative;
+}
+
+.mainContainer {
+ --nav-height : 95px;
+ /* --max-width : 1920px; */
+ --max-width : 100vw;
+ --max-height : 100vh;
+ --leftContiner-width : 466px;
+ --rightContiner-width : calc( var(--max-width) - var(--leftContiner-width));
+ /* --height : calc(100vh - 56px); */
+ --height : calc(var(--max-height) - var(--nav-height));
+
+
+ display: grid;
+ grid-template-columns: var(--leftContiner-width) var(--rightContiner-width);
+ height: var(--height);
+ /* overflow: hidden; */
+}
+
+.basicFont {
+ color: #8f8f8f;
+ font-family: 'NanumSquare';
+ font-weight: 600;
+ font-size: 20px;
+}
+
+.subLeftContiner {
+ --title-width : 322px;
+
+ height: 100%;
+ background-color: #fbf7f2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.title {
+ font-family: 'BMJUA';
+ color: #ff7f3f;
+ width: var(--title-width);
+ margin-top: 44px;
+ margin-bottom: 44px;
+ font-size: 48px;
+}
+
+.profileImageContainer {
+ width: var(--title-width);
+ height: var(--title-width);
+ border-radius: 50%;
+ margin-bottom: 18px;
+ /* border: 1px solid #dcdcdc; */
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+}
+
+.profileImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.transparentButton {
+ background: transparent;
+ border: none;
+ margin-top: 28px;
+ padding: 0px;
+ transition: color 0.2s ease-out;
+}
+
+.transparentButton:hover {
+ color: #ff7f3f;;
+}
+
+.selected {
+ color: #ff7f3f;
+}
+
+.subRightContiner {
+ --rightContainer-padding-left : 80px;
+ --rightContainer-padding-top : 64px;
+ --rightContainer-column-width : calc((var(--rightContiner-width) - var(--rightContainer-padding-left)) / 2);
+
+ display: grid;
+ /* grid-template-columns: var(--rightContainer-column-width) var(--rightContainer-column-width); */
+ grid-template-columns: 45% 45%;
+ grid-template-rows: repeat(4, minmax(160px, 160px));
+ overflow: hidden;
+ padding-top: var(--rightContainer-padding-top);
+ padding-left: var(--rightContainer-padding-left);
+}
+
+.alertContainer {
+ position: absolute;
+
+}
+
+.formGroup {
+ padding-right: var(--rightContainer-padding-left);
+}
+
+.formLabel {
+ margin: 0px 0px 8px 0px;
+ padding: 0px;
+}
+
+.formControl {
+ height: 50px;
+ border-color: #d7d7d7;
+ border-width: 2px;
+ border-radius: 14px;
+
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.genderFormContainer {
+ padding-right: var(--rightContainer-padding-left);
+}
+
+.maleButton {
+ /* width: 250px; */
+ width: 46%;
+
+ height: 46px;
+ border: 2px solid #d7d7d7;
+ border-radius: 14px;
+ background-color: white;
+ /* margin-right: 18px; */
+ margin-right: 4%;
+
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+ transition: all 0.2s ease-out;
+}
+
+.maleSelected,
+.maleButton:hover {
+ background-color: #c0dfff;
+ color: black;
+}
+
+.femaleButton {
+ /* width: 250px; */
+ width: 46%;
+ height: 46px;
+ border: 2px solid #d7d7d7;
+ border-radius: 14px;
+ background-color: white;
+ /* margin-left: 18px; */
+ margin-left: 4%;
+
+
+ color: #6d6d6d;
+ font-family: 'NanumSquare';
+ font-size: 18px;
+ font-weight: 600;
+ transition: all 0.2s ease-out;
+}
+
+.femaleSelected,
+.femaleButton:hover {
+ background-color: #fca8a5;
+ color: black;
+}
+
+.submitWarpper {
+ text-align: right;
+ padding-right: var(--rightContainer-padding-left);
+}
+
+.submitButton {
+ height: 60px;
+ width: 128px;
+ color: #606060;
+ font-weight: 800;
+ background-color: #ffa978;
+ background-color: cutom-color;
+ border-radius: 14px;
+ border-width: 0px;
+ transition: all 0.2s ease-out;
+}
+
+.submitButton:hover {
+ background-color: #ff9878;
+}
+
+.submitButton:active {
+ background-color: #ff8f4f;
+}
\ No newline at end of file
diff --git a/style/src/components/MyPage/MyPageAlert.js b/style/src/components/MyPage/MyPageAlert.js
new file mode 100644
index 0000000..be3903a
--- /dev/null
+++ b/style/src/components/MyPage/MyPageAlert.js
@@ -0,0 +1,35 @@
+import AlertMessage from '../common/AlertMessage'
+
+function MyPageAlert({ errorCode, isShowAlert, setIsShowAlert }) {
+ const errorCodeList = ['UPDATE_DONE', 'DuplicateKey']
+ return (
+ <>
+ {errorCode === 'UPDATE_DONE' && (
+
+ )}
+ {errorCode === 'DuplicateKey' && (
+
+ )}
+ {!errorCodeList.includes(errorCode) && (
+
+ )}
+ >
+ )
+}
+
+export default MyPageAlert
diff --git a/style/src/components/MyPage/MyPrivacy.js b/style/src/components/MyPage/MyPrivacy.js
new file mode 100644
index 0000000..78ae74c
--- /dev/null
+++ b/style/src/components/MyPage/MyPrivacy.js
@@ -0,0 +1,175 @@
+import ClassMerger from '../common/ClassNameGenerater'
+import styles from './MyPage.module.css'
+import FormBox from './FormBox'
+import { useEffect, useRef, useState } from 'react'
+import authenticatedAxios from '../../api/authenticatedAxios'
+import { API_URL } from '../../api/apiConfig'
+import { updateUser } from '../User/UpdateInfo.js'
+
+function MyPrivacy({ userId, showAlert, setErrorCode }) {
+ const [name, setName] = useState('')
+ const [gender, setGender] = useState('')
+ const [email, setEmail] = useState('')
+ const [phone, setPhone] = useState('')
+
+ const user = useRef({})
+
+ const [isNameInvalid, setIsNameInvalid] = useState(false)
+ const [isEmailInvalid, setIsEmailInvalid] = useState(false)
+ const [isPhoneInvalid, setIsPhoneInvalid] = useState(false)
+
+ const maleButtonHandler = (event) => {
+ if (gender !== 'male') {
+ setGender('male')
+ }
+ }
+ const femaleButtonHandler = (event) => {
+ if (gender !== 'female') {
+ setGender('female')
+ }
+ }
+
+ // 입력갑 검사 및 제출
+ const onSubmit = async (event) => {
+ event.preventDefault()
+
+ let modifyUser = {}
+
+ if (/^[가-힣a-zA-Z\s]+$/.test(name)) {
+ setIsNameInvalid(false)
+ // user.current.name = name
+ modifyUser.name = name
+ } else {
+ setIsNameInvalid(true)
+ }
+ if (
+ /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i.test(
+ email
+ )
+ ) {
+ setIsEmailInvalid(false)
+ // user.current.email = email
+ modifyUser.email = email
+ } else {
+ setIsEmailInvalid(true)
+ }
+ if (/^\d{3}-\d{4}-\d{4}$/.test(phone)) {
+ setIsPhoneInvalid(false)
+ // user.current.phoneNumber = phone
+ modifyUser.phoneNumber = phone
+ } else {
+ setIsPhoneInvalid(true)
+ }
+ if (!isNameInvalid && !isEmailInvalid && !isPhoneInvalid) {
+ modifyUser.userId = userId
+ modifyUser.gender = gender
+ updateUser(modifyUser).then((result) => {
+ setErrorCode(result.code)
+ showAlert()
+ })
+ }
+ }
+
+ // 신체 정보 fetch
+ useEffect(() => {
+ const fetchSize = async () => {
+ try {
+ const response = await authenticatedAxios.get(
+ `${API_URL}/userInfo/api/info`,
+ { params: { userId: userId } }
+ )
+ if (response.status === 200 || response.status === 201) {
+ user.current = response.data.user
+ setName(response.data.user.name)
+ setEmail(response.data.user.email)
+ setGender(response.data.user.gender)
+ setPhone(response.data.user.phoneNumber)
+
+ return { success: true, result: response.data }
+ } else {
+ return { success: false, error: 'Size Fetch Failed.' }
+ }
+ } catch (error) {
+ if (error.response && error.response.status >= 500) {
+ return { success: false, error: 'Server error occurred.' }
+ } else {
+ return { success: false, error: error.message }
+ }
+ }
+ }
+ if (userId) fetchSize()
+ }, [userId])
+ return (
+
+ )
+}
+
+export default MyPrivacy
diff --git a/style/src/components/MyPage/MySize.js b/style/src/components/MyPage/MySize.js
new file mode 100644
index 0000000..b7637d4
--- /dev/null
+++ b/style/src/components/MyPage/MySize.js
@@ -0,0 +1,178 @@
+import ClassMerger from '../common/ClassNameGenerater'
+import styles from './MyPage.module.css'
+import FormBox from './SizeFormBox'
+import { useState, useEffect } from 'react'
+
+import authenticatedAxios from '../../api/authenticatedAxios'
+import { API_URL } from '../../api/apiConfig'
+import { updateSize } from '../User/UpdateInfo'
+
+function MySize({ userId, showAlert, setErrorCode }) {
+ const [height, setHeight] = useState(0)
+ const [weight, setWeight] = useState(0)
+ const [shoulderWidth, SetshoulderWidth] = useState(0)
+ const [chestWidth, setChestWidth] = useState(0)
+ const [length, setLength] = useState(0)
+ // 저장한 후 sizeAPI로 변경된 데이터 다시 받아오게 함
+ const [reFetchTrigger, setReFetchTrigger] = useState(0)
+
+ const [isHeightInvalid, setIsHeightInvalid] = useState(0)
+ const [isWeightInvalid, setIsWeightInvalid] = useState(0)
+
+ const onSubmit = async (event) => {
+ event.preventDefault()
+
+ let modifySize = {}
+ // 키 체크
+ if (height) {
+ setIsHeightInvalid(false)
+ modifySize.height = height
+ } else {
+ setIsHeightInvalid(true)
+ }
+ // 몸무게 체크
+ if (weight) {
+ setIsWeightInvalid(false)
+ modifySize.weight = weight
+ } else {
+ setIsWeightInvalid(true)
+ }
+ // 어깨너비
+ if (shoulderWidth) {
+ modifySize.shoulderWidth = shoulderWidth
+ }
+ // 가슴단면
+ if (chestWidth) {
+ modifySize.chestWidth = chestWidth
+ }
+ // 총장
+ if (length) {
+ modifySize.length = length
+ }
+ modifySize.userId = userId
+ updateSize(modifySize).then((result) => {
+ setErrorCode(result.code)
+ showAlert()
+ setReFetchTrigger((curr) => curr + 1)
+ })
+ }
+
+ // 신체 정보 fetch
+ // DB 제대로 만들어지면 어떻게 될 지 몰라서 일단 2번 fetch하게 만듦
+ useEffect(() => {
+ const fetchSize1 = async () => {
+ try {
+ const response = await authenticatedAxios.get(
+ `${API_URL}/userInfo/api/size`,
+ { params: { userId: userId } }
+ )
+ if (response.status === 200 || response.status === 201) {
+ setLength(response.data.message.length)
+ SetshoulderWidth(response.data.message.shoulderWidth)
+ setChestWidth(response.data.message.chestWidth)
+ return { success: true, result: response.data }
+ } else {
+ return { success: false, error: 'Size Fetch Failed.' }
+ }
+ } catch (error) {
+ if (error.response && error.response.status >= 500) {
+ return { success: false, error: 'Server error occurred.' }
+ } else {
+ return { success: false, error: error.message }
+ }
+ }
+ }
+ const fetchSize2 = async () => {
+ try {
+ const response = await authenticatedAxios.get(
+ `${API_URL}/userInfo/api/info`,
+ { params: { userId: userId } }
+ )
+ if (response.status === 200 || response.status === 201) {
+ setHeight(response.data.user.height)
+ setWeight(response.data.user.weight)
+ return { success: true, result: response.data }
+ } else {
+ return { success: false, error: 'Size Fetch Failed.' }
+ }
+ } catch (error) {
+ if (error.response && error.response.status >= 500) {
+ return { success: false, error: 'Server error occurred.' }
+ } else {
+ return { success: false, error: error.message }
+ }
+ }
+ }
+ if (userId) {
+ fetchSize1()
+ fetchSize2()
+ }
+ console.log('fetch')
+ }, [userId, reFetchTrigger])
+
+ return (
+
+ )
+}
+
+export default MySize
diff --git a/style/src/components/MyPage/RightSubMyPage.js b/style/src/components/MyPage/RightSubMyPage.js
new file mode 100644
index 0000000..93a512d
--- /dev/null
+++ b/style/src/components/MyPage/RightSubMyPage.js
@@ -0,0 +1,45 @@
+import Proptypes from 'prop-types'
+import MyPrivacy from './MyPrivacy'
+import MySize from './MySize'
+import { useSelector } from 'react-redux'
+
+function RightSubMyPage({ page, showAlert, setErrorCode }) {
+ // 새로고침 or 제출 버튼 누르면 웹 페이지 리프레쉬 되면서 redux 초기화 됨
+ // redux 초기화 되면서 user에 저장된 데이터 없어져서 오류 발생.
+ const user = useSelector((state) => state.auth.user)
+
+ switch (page) {
+ case 'privacy':
+ return (
+
+ )
+ case 'size':
+ return (
+
+ )
+ // case 'closet':
+ // return (
+ // <>옷장 - 어떻게 해야할 지 생각 중. 무슨 데이터가 들어가야 할까요?>
+ // )
+ // case 'style':
+ // return (
+ // <>스타일 - 어떻게 해야할 지 생각 중. 무슨 데이터가 들어가야 할까요?>
+ // )
+ default:
+ return <>error!>
+ }
+}
+
+RightSubMyPage.propTypes = {
+ page: Proptypes.string.isRequired,
+}
+
+export default RightSubMyPage
diff --git a/style/src/components/MyPage/SizeFormBox.js b/style/src/components/MyPage/SizeFormBox.js
new file mode 100644
index 0000000..ca25f42
--- /dev/null
+++ b/style/src/components/MyPage/SizeFormBox.js
@@ -0,0 +1,43 @@
+import ClassMerger from '../common/ClassNameGenerater'
+import { Form } from 'react-bootstrap'
+import styles from './MyPage.module.css'
+
+function FormBox({
+ id,
+ label,
+ value,
+ min,
+ max,
+ setState,
+ placeholder,
+ isInvalid = false,
+ invalidTest,
+}) {
+ return (
+
+
+ {label}
+
+ {
+ const input = event.target.value
+ const isValid = /^\d+(\.\d{0,1})?$/.test(input) || input === ''
+ if (isValid && min <= input && input < max) {
+ const floatInput = parseFloat(input)
+ setState(isNaN(floatInput) ? 0 : floatInput)
+ }
+ }}
+ />
+
+ {invalidTest}
+
+
+ )
+}
+
+export default FormBox
diff --git a/style/src/components/MyPage/getUserInfo.js b/style/src/components/MyPage/getUserInfo.js
new file mode 100644
index 0000000..e69de29
diff --git a/style/src/components/Navigationbar/Nav.js b/style/src/components/Navigationbar/Nav.js
index 64ee4cc..220e872 100644
--- a/style/src/components/Navigationbar/Nav.js
+++ b/style/src/components/Navigationbar/Nav.js
@@ -52,13 +52,13 @@ function Navigation() {
피팅
-
코디 추천
-
+ */}
-
-
-
- )
-}
-export default RecommendPage
diff --git a/style/src/components/RecommendPage/RecommendPage.css b/style/src/components/RecommendPage/RecommendPage.css
new file mode 100644
index 0000000..e421747
--- /dev/null
+++ b/style/src/components/RecommendPage/RecommendPage.css
@@ -0,0 +1,36 @@
+.recommend-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ min-height: calc(100vh - 100px);
+ background-color: #f8f9fa;
+ }
+
+ h2 {
+ font-size: 2rem;
+ color: #333;
+ margin-top: 2rem;
+ margin-bottom: 0.5rem;
+ text-align:center;
+ }
+
+ p {
+ font-size: 1.25rem;
+ color: #666;
+ text-align:center;
+ }
+
+ h3 {
+ font-size: 1rem;
+ color: #999;
+ margin-top: 1rem;
+ font-style: italic;
+ text-align:center;
+ }
+
+ .coming-soon-image {
+ width: 100%;
+ max-width: 400px;
+ margin-top: 2rem;
+ }
\ No newline at end of file
diff --git a/style/src/components/RecommendPage/RecommendPage.js b/style/src/components/RecommendPage/RecommendPage.js
new file mode 100644
index 0000000..5d78ffb
--- /dev/null
+++ b/style/src/components/RecommendPage/RecommendPage.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import Navigation from '../Navigationbar/Nav'
+import Footer from '../Footer'
+import './RecommendPage.css'
+import comingSoonImage from '../../images/commingsoon.png'
+
+function RecommendPage() {
+ return (
+
+
+
+
추천 시스템이 곧 도입됩니다.
+
최고의 제품을 추천 받아보세요.
+
여러분께 딱 맞는 아이템을 찾아드립니다.
+
COMING SOON...
+

+
+
+
+ )
+}
+export default RecommendPage
diff --git a/style/src/components/User/Login.js b/style/src/components/User/Login.js
index 41a4a03..63216bb 100644
--- a/style/src/components/User/Login.js
+++ b/style/src/components/User/Login.js
@@ -28,8 +28,7 @@ function LoginPage({ onClose }) {
password,
})
if (response.status === 200 || response.status === 201) {
- localStorage.setItem('token', response.data.token)
- dispatch(login(response.data.user))
+ dispatch(login(response.data))
onClose()
}
} catch (error) {
diff --git a/style/src/components/User/Logout.js b/style/src/components/User/Logout.js
index 7b61d3e..a4b61f0 100644
--- a/style/src/components/User/Logout.js
+++ b/style/src/components/User/Logout.js
@@ -8,6 +8,7 @@ function Logout() {
const dispatch = useDispatch()
const handleLogout = () => {
dispatch(logout())
+ localStorage.removeItem('user')
localStorage.removeItem('token')
alert('정상적으로 로그아웃 되었습니다.')
}
diff --git a/style/src/components/User/Register.js b/style/src/components/User/Register.js
index 7042e55..08c1bcf 100644
--- a/style/src/components/User/Register.js
+++ b/style/src/components/User/Register.js
@@ -14,6 +14,8 @@ async function registerUser(userData) {
if (userData.file) {
formData.append('file', userData.file)
+ } else {
+ return { success: false, error: '이미지를 업로드 해주세요.' }
}
try {
@@ -27,16 +29,22 @@ async function registerUser(userData) {
}
)
- if (response.status === 200 || response.status === 201) {
+ if (response.status >= 200 && response.status < 300) {
return { success: true, result: response.data }
} else {
- return { success: false, error: 'Registration failed.' }
+ const errorData = await response.json()
+ return {
+ success: false,
+ error: errorData.error || 'Registration failed.',
+ }
}
} catch (error) {
- if (error.response && error.response.status >= 500) {
- return { success: false, error: 'Server error occurred.' }
+ if (error.response) {
+ return { success: false, error: error.response.data.error }
+ } else if (error.request) {
+ return { success: false, error: 'Network error occurred.' }
} else {
- return { success: false, error: error.message }
+ return { success: false, error: 'An unknown error occurred.' }
}
}
}
diff --git a/style/src/components/User/SignupPage.css b/style/src/components/User/SignupPage.css
index 8f7dde1..8051912 100644
--- a/style/src/components/User/SignupPage.css
+++ b/style/src/components/User/SignupPage.css
@@ -24,6 +24,18 @@
border: 1px solid gray;
width: 300px;
}
+
+ .email-check-button,
+ .email-send-button {
+ padding: 0.5rem 1rem;
+ width: 300px;
+ background-color: #0d6efd;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1.2rem;
+ }
.size-input-group {
display: flex;
diff --git a/style/src/components/User/SignupPage.js b/style/src/components/User/SignupPage.js
index 645d49b..d2d9106 100644
--- a/style/src/components/User/SignupPage.js
+++ b/style/src/components/User/SignupPage.js
@@ -1,4 +1,4 @@
-import React, { useState, useRef } from 'react'
+import React, { useState, useRef, useEffect } from 'react'
import { Tooltip } from 'react-tooltip'
import { Modal, Button } from 'react-bootstrap'
import ImageUploader from './ImageUploader'
@@ -15,11 +15,14 @@ import {
validateInteger,
} from './Validations'
+import LoadingIndicator from '../LoadingIndicator'
+
function SignupPage({ onClose }) {
const dispatch = useDispatch()
const [name, setName] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const [email, setEmail] = useState('')
+ const [emailAuthCode, setEmailAuthCode] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [gender, setGender] = useState('')
@@ -39,6 +42,7 @@ function SignupPage({ onClose }) {
const [nameError, setNameError] = useState('')
const [phoneError, setPhoneError] = useState('')
const [emailError, setEmailError] = useState('')
+ const [emailAuthError, setEmailAuthError] = useState('')
const [passwordError, setPasswordError] = useState('')
const [confirmPasswordError, setConfirmPasswordError] = useState('')
const [heightError, setHeightError] = useState('')
@@ -63,6 +67,10 @@ function SignupPage({ onClose }) {
}, 500)
}
+ // 이메일 중복 검사
+ const [loading, setLoading] = useState(false)
+ const [errorMessage, setErrorMessage] = useState('')
+
const handleNameChange = (e) => {
setName(e.target.value)
onBlurHandler(e, validateName, setNameError)
@@ -185,6 +193,7 @@ function SignupPage({ onClose }) {
if (canSubmitted) {
try {
+ setLoading(true)
const userData = {
email,
name,
@@ -201,13 +210,17 @@ function SignupPage({ onClose }) {
if (registerResult.success) {
console.log('success')
const { token, user } = registerResult.result
- localStorage.setItem('token', token)
dispatch(login({ token, user }))
onClose()
+ } else {
+ setErrorMessage(registerResult.error)
+ alert(registerResult.error)
}
} catch (error) {
console.log('registerfail')
handleInvalidForm()
+ } finally {
+ setLoading(false)
}
} else {
console.log('submitfail')
@@ -217,283 +230,309 @@ function SignupPage({ onClose }) {
}
return (
-
-
- 회원가입
-
-
-