Skip to content

Commit

Permalink
Merge pull request #29 from youngwan2/signin
Browse files Browse the repository at this point in the history
feature/회원가입 시 이용약관 및 개인정보처리 동의 기능 추가
  • Loading branch information
youngwan2 authored Mar 27, 2024
2 parents 61beaf1 + adad2d9 commit 5ddc6a9
Show file tree
Hide file tree
Showing 20 changed files with 434 additions and 186 deletions.
9 changes: 7 additions & 2 deletions src/app/(policy)/policy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
"use client"

import BackMoveButton from "@/components/UI/common/BackMoveButton";
import PolicyTaps from "@/components/UI/policy/PolicyTaps";
import PrivacyPolicy from "@/components/UI/policy/PrivacyPolicy";
import PrivacyPolicyConsent from "@/components/UI/policy/PrivatePolicyConsent";
import TermsConditions from "@/components/UI/policy/TermsConditions";
import { useState } from "react";
import { usePolicyTaps } from "@/store/store";

export default function PolicyPage(){

const [tapNum, setTapNum] = useState(0)
const tapNum = usePolicyTaps((state)=> state.tapNum)
const setTapNum = usePolicyTaps((state)=> state.setTapNum)

function onClickChangeTap(num:number){
setTapNum(num)
}

return(
<>
<BackMoveButton/>
<PolicyTaps tapNum ={tapNum} onClickChangeTap={onClickChangeTap}/>
{tapNum === 0 && <TermsConditions/> }
{tapNum === 1 && <PrivacyPolicy/> }
{tapNum === 2 && <PrivacyPolicyConsent/> }
</>
)
}
31 changes: 25 additions & 6 deletions src/app/api/auth/general-auth/signin/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import bcrpt from 'bcrypt'
import { NextRequest, NextResponse } from 'next/server'
import { openDB } from '@/utils/connect'
import { userSchema } from '@/validation/joi/schema'
import { consentSchema, userSchema } from '@/validation/joi/schema'
import { HTTP_CODE } from '@/app/http-code'

// 암호화 설정(옵션)
Expand All @@ -13,7 +13,8 @@ export async function POST(req: NextRequest) {

// 1. 유효성 조건식 작성
const { '0': body } = await req.json()
const { email, password, reConfirmPw } = body
const { email, password, reConfirmPw, consents } = body


// 2. 유효성 검증
const { error, value } = userSchema.validate({
Expand All @@ -22,24 +23,42 @@ export async function POST(req: NextRequest) {
reConfirmPw,
})

const { error: vaiidConcentError, value: vaildConcents } = consentSchema.validate({
...consents
})


// 3. 검증 실패 시 처리
if (error) {
if (error || vaiidConcentError) {
return NextResponse.json({
...HTTP_CODE.BAD_REQUEST,
meg: error.details,
meg: error?.details || vaiidConcentError?.details,
})
}

// 4. 검증 성공 시 비밀번호 해시 암호화
const { email: validEmail, password: validPs } = value
const { all, term, private: privateConsent, child, event } = vaildConcents

bcrpt.hash(validPs, SALT, async function (err, hash) {
const query = `
const userInsertQuery = `
INSERT INTO users(email, password)
VALUES ($1, $2)
`
db.query(query, [validEmail, hash])
const userSelectQuery = `
SELECT user_id FROM users
WHERE email = $1
`

const consentInsertQuery = `
INSERT INTO consents(user_id, all_consent, terms_consent, private_consent, child_consent, event_consent)
VALUES ($1, $2, $3, $4, $5, $6)
`

await db.query(userInsertQuery, [validEmail, hash])
const result = await db.query(userSelectQuery, [email])
const { user_id } = result.rows[0] || { user_id: null }
if (user_id) await db.query(consentInsertQuery, [user_id, all, term, privateConsent, child, event])
await db.end()
})

Expand Down
2 changes: 1 addition & 1 deletion src/components/UI/auth/login/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function LoginForm() {
}

function onClickBackMove() {
router.back()
router.push('/')
}

return (
Expand Down
71 changes: 71 additions & 0 deletions src/components/UI/auth/signin/Consent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ConsentsType } from "@/types/items.types"
import { ChangeEvent, MouseEvent, useState } from "react"
import PolicyModal from "./PolicyModal"
import { toast } from "react-toastify"
import { createPortal } from "react-dom"

interface ProsType {
consents: ConsentsType
setConsents: ({ }: any) => void
}

const checkBoxStyle = `
before:absolute before:top-[55%] before:translate-y-[-45%] before:left-[50%] before:translate-x-[-50%]
relative hover:cursor-pointer shadow-[0_0_0_1px_gray] w-[18px] h-[18px] inline-block transition-all
`

const consentItems = [
{ id: 'all', label: '전체동의' },
{ id: 'term', label: '이용약관(필수)', href: '/policy' },
{ id: 'private', label: '개인정보 이용 및 수집 동의(필수)', href: '/policy' },
{ id: 'child', label: '만 14세 이상(필수)' },
{ id: 'event', label: '이벤트 등의 프로모션 메일 수신 동의' }
];

export default function Consent({ setConsents, consents }: ProsType) {

const [isOpenModal, setIsOpenModal] = useState({ term: false, private: false })

function openModal(target: string) {
target === 'term' && setIsOpenModal({ ...isOpenModal, term: !isOpenModal.term })
target === 'private' && setIsOpenModal({ ...isOpenModal, private: !isOpenModal.private })
}

function onClickAgreement(target: string) {
openModal(target)
target === 'term' && setConsents({ ...consents, term: true })
target === 'private' && setConsents({ ...consents, private: true })

}

function onChangeCheck(e: ChangeEvent<HTMLElement> | MouseEvent<HTMLSpanElement>) {
const type = e.currentTarget.dataset.id
const { all, term, private: privateConsent, child, event } = consents
if (type === 'all') return setConsents({ all: !all, term: !all, private: !all, child: !all, event: !all })
if (type === 'term') return setConsents({ ...consents, term: !term })
if (type === 'private') return setConsents({ ...consents, private: !privateConsent })
if (type === 'child') return setConsents({ ...consents, child: !child })
if (type === 'event') setConsents({ ...consents, event: !event })
}

return (
<>
<article aria-label="약관동의 체크박스" className="max-w-[450px] w-full mx-auto bg-white rounded-[2px] p-[10px] mt-[2em] shadow-[inset_3px_3px_5px_rgba(0,0,0,0.5)] text-gray-700">
{consentItems.map(item => (
<div key={item.id} className="py-[0.5em] flex items-center">
<label className="w-[300px] inline-block font-bold"> {item.id === 'term' || item.id === 'private' ? <span onClick={() => openModal(item.id)} className="text-black shadow-[inset_0_-1px_0_0_green] hover:cursor-pointer">{item.label}</span> : item.label} </label>
<input onChange={onChangeCheck} className="hidden" checked={consents[item.id]} data-id={item.id} type="checkbox" name={`${item.id}-consent`} />
<span
onClick={onChangeCheck}
data-id={item.id}
className={`${consents[item.id] ? 'before:content-["✔"] before:opacity-100 text-green-600 ' : 'before:content-[""] before:opacity-0'} ${checkBoxStyle}`}
></span>
</div>
))}
</article>
{createPortal(
<PolicyModal isOpenModal={isOpenModal} onClickAgreement={onClickAgreement} />, document.body
)}
</>
);
};
8 changes: 4 additions & 4 deletions src/components/UI/auth/signin/EmailValidityMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ export default function EmailValidityMessage({
}) {
if (isEmail)
return (
<span className="font-sans block px-[5px] text-[#56e146] text-[14.3px] ml-[0.5em]">
- 이메일 형식과 일치합니다.{' '}
<span className="font-sans inline-block px-[5px] text-[#56e146] text-[14.3px] ml-[3.2em]">
이메일 형식과 일치합니다.{' '}
</span>
)

if (!isEmail)
return (
<span className=" font-sans text-[#f25555] block ml-[0.5em] text-[14.3px]">
- 이메일 형식과 일치시키세요{' '}
<span className=" font-sans text-[#f25555] ml-[3.6em] text-[14.3px] inline-block">
이메일 형식과 일치시키세요{' '}
</span>
)
}
35 changes: 35 additions & 0 deletions src/components/UI/auth/signin/PolicyModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import PrivacyPolicyConsent from "../../policy/PrivatePolicyConsent";
import TermsConditions from "../../policy/TermsConditions";

interface PropsType {
isOpenModal: {
term: boolean
private: boolean
}
onClickAgreement: (target: string) => void
}


export default function PolicyModal({ isOpenModal, onClickAgreement }: PropsType) {
return (
<>
{
isOpenModal.term && <div className="rounded-[10px] fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] max-h-[85vh] w-[90%] overflow-auto bg-white shadow-[0_0_20px_10px_rgba(100,100,125,0.3)] z-[10000]">
<h2 className="text-[1.25em] absolute top-[0.5em] left-[0.5em] font-bold font-sans">서비스 이용약관</h2>
<TermsConditions />
<button onClick={() => onClickAgreement('term')} className="w-full bg-[tomato] text-white p-[5px] hover:bg-[#f0735c]">확인</button>
<button onClick={() => onClickAgreement('private')} className="w-full bg-[#ffffff] text-black text-[0.95em] p-[5px] font-sans">※ 거부시 체크박스를 직접 해제 해주시면 됩니다. 단, 회원가입을 더 이상 진행할 수 없습니다.</button>
</div>
}
{
isOpenModal.private && <div className="rounded-[10px] fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] max-h-[85vh] w-[90%] overflow-auto bg-white shadow-[0_0_20px_10px_rgba(100,100,125,0.3)] z-[10000]">
<h2 className="text-[1.25em] absolute top-[0.5em] left-[0.5em] font-bold font-sans">개인정보 수집 및 이용 동의서</h2>
<PrivacyPolicyConsent />
<button onClick={() => onClickAgreement('private')} className="w-full bg-[tomato] text-white p-[5px] hover:bg-[#f0735c]">확인</button>
<button onClick={() => onClickAgreement('private')} className="w-full bg-[#ffffff] text-black text-[0.95em] p-[5px] font-sans">※ 거부시 확인 후 체크박스를 직접 해제 해주시면 됩니다. 단, 회원가입을 더 이상 진행할 수 없습니다.</button>
</div>
}

</>
)
}
42 changes: 35 additions & 7 deletions src/components/UI/auth/signin/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ import { reqSingIn } from '@/services/user/post'
import BackButton from '../common/BackButton'
import SignInWarnModal from './SignInWarnModal'
import FormTitle from '../common/FormTitle'
import Consent from './Consent'
import toast from 'react-hot-toast'
import { ConsentsType } from '@/types/items.types'

export default function SignInForm() {
const [isEmail, setIsEmail] = useState(false)
const [isPassword, setIsPassword] = useState(false)
const [isReconfirmPassword, setIsReconfirmPassword] = useState(false)
const [isSuccess, setisSuccess] = useState(false)
const [consents, setConsents] = useState({
all: false,
term: false,
private: false,
child: false,
event: false


})
const [existsEmail, setExistsEmail] = useState(false)

const [email, setEmail] = useState('')
Expand All @@ -35,20 +47,23 @@ export default function SignInForm() {
if (isSuccess) return router.push('/login')
}, [isSuccess, router])

// onClick | 로그인 요청
// onClick | 회원가입 요청
async function onClickReqSingin() {
router.prefetch('/')
const isSuccess =
(await reqSingIn({ email, password, reConfirmPw })) || null
const isAgreementConcent = consentCheck(consents)
if(!isAgreementConcent) return toast('작업을 완료하려면 모든 필수 동의사항에 동의해야 합니다.')

const isSuccess = await reqSingIn({ email, password, reConfirmPw }, consents)

if (isSuccess) {
setExistsEmail(false)
setisSuccess(true)
router.push('/')
toast.success('승인되었습니다.')
}
}

function onClickBack() {
router.back()
router.push('/login')
}

function onClickModalClose() {
Expand All @@ -60,7 +75,7 @@ export default function SignInForm() {
return (
<form
ref={formRef}
className="rounded-[10px] flex flex-col fixed max-w-[480px] px-[5px] min-h-[350px] left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-[100%] shadow-[inset_0_0_0_2px_white] "
className="sm:max-h-[100%] max-h-[650px] backdrop-blur-[5px] rounded-[10px] flex flex-col fixed max-w-[550px] px-[5px] min-h-[350px] left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-[100%] shadow-[inset_0_0_0_2px_white] overflow-y-auto "
onSubmit={onSubmit}
>
<FormTitle> 회원가입</FormTitle>
Expand Down Expand Up @@ -95,13 +110,26 @@ export default function SignInForm() {
setReConfirmPw={setReConfirmPw}
/>

{/* 약관 동의 */}
<Consent consents={consents} setConsents={setConsents} />


{/* 전송버튼 */}
<SignInSubmitButton
isDisabled={isSuccess}
existsEmail={existsEmail}
isVaildForm={isVaildForm}
existsEmail={existsEmail}
onClick={onClickReqSingin}
/>
</form>
)
}

// 필수 약관에 동의하였는지 체크
function consentCheck(consents: ConsentsType) {
const { term, child, private: privateConsent } = consents

if (term && child && privateConsent) {
return true
} else { return false }
}
12 changes: 6 additions & 6 deletions src/components/UI/auth/signin/SignInPasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ export default function SignInPasswordInput({
/>
</div>
{isPassword ? (
<span className="font-sans block px-[5px] text-[#56e146] ml-[0.5em] text-[14.3px]">
- 패스워드 형식과 일치합니다.
<span className="font-sans px-[5px] text-[#56e146] ml-[3.3em] text-[14.3px] inline-block">
패스워드 형식과 일치합니다.
</span>
) : (
<>
<span className="font-sans text-[#837f7f] block ml-[0.5em] text-[14.3px] ">
- 특수문자 1개 이상, 문자 및 숫자 1개 이상 포함한 8자 이상
<span className="font-sans text-[#837f7f] ml-[3.6em] text-[14.3px] inline-block ">
특수문자 1개 이상, 문자 및 숫자 1개 이상 포함한 8자 이상
</span>
<span className=" font-sans text-[#f25555] block ml-[0.5em] text-[14.3px]">
- 패스워드 형식과 일치시키세요
<span className=" font-sans text-[#f25555] ml-[3.6em] text-[14.3px] inline-block">
패스워드 형식과 일치시키세요
</span>
</>
)}
Expand Down
8 changes: 4 additions & 4 deletions src/components/UI/auth/signin/SignInPwReConfirmInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export default function SignInPasswordReConfirmInput({
/>
</div>
{isReconfirmPassword ? (
<span className="block px-[5px] text-[#56e146] ml-[0.5em] font-sans text-[14.3px]">
- 패스워드 형식과 일치합니다.
<span className="font-sans px-[5px] text-[#56e146] ml-[3.3em] text-[14.3px] inline-block">
패스워드 형식과 일치합니다.
</span>
) : (
<>
<span className=" text-[#f25555] block ml-[0.5em] font-sans text-[14.3px]">
- 앞서 작성한 패스워드와 일치시키세요
<span className="font-sans text-[#f25555] ml-[3.6em] text-[14.3px] inline-block">
앞서 작성한 패스워드와 일치시키세요
</span>
</>
)}
Expand Down
Loading

0 comments on commit 5ddc6a9

Please sign in to comment.