반응형

술래는 본래 기획이 커뮤니티였다. 서로 자신의 레시피를올리고 자기가 가진 재료로 만들수있는 다양한 칵테일을 검색할수있는 기능이 원래 생각한 기능이었다. 현재는 칵테일레시피를 올리고 저장할수는 있지만 의견을 나눌수가 없다. 그래서 커뮤니티의 시작이자 끝인 댓글 기능을 추가해보겠다. 

 

우선은 댓글을 입력하고 보여주기위해서 html코드로 디자인을 잡아주었다. 

아예 처음부터만들기에는 나의 미적감각이 절망적인 수준이어서 FlowVite라는 사이트에서 코드를 보고 컴포넌트화 했다.

 

https://flowbite.com/blocks/publisher/comments/

 

flowbite.com

 

쪼개다 보니 생각보다 컴포넌트가 많아졌다.

그림으로 그려보면 이렇게 나온다.

 

CommentBox.tsx

import React from 'react'
import DisccussionCount from './DisccussionCount';
import WriteCommentText from './WriteCommentText';
import CommentTop from './CommentTop';

function CommentBox() {
  return (
    <section className="bg-white dark:bg-gray-900 py-8 lg:py-16 antialiased">
      <div className="mx-auto px-4">
        <DisccussionCount></DisccussionCount>
        <WriteCommentText></WriteCommentText>
        <CommentTop></CommentTop>
        <article className="p-6 mb-3 ml-6 lg:ml-12 text-base bg-white rounded-lg dark:bg-gray-900">
          <CommentTop></CommentTop>
        </article>  
        {/* 밑에 달린 코멘트는 이 태그에 들어감 2번쨰글 부터 border-t 로 구분선 추가. */}
        <div className='border-t'></div>
        <CommentTop></CommentTop>
      </div>
    </section>
  )
}

export default CommentBox

 

DisscussionCount.tsx

import React from 'react'

function DisccussionCount() {
  return (
    <div className="flex justify-between items-center mb-6">
        <h2 className="text-lg lg:text-2xl font-bold text-gray-900 dark:text-white">Discussion (20)</h2>
    </div>
  )
}

export default DisccussionCount

 

WriteCommentText.tsx

import { useLoginContext } from '@/app/(context)/LoginContext';
import React from 'react'

function WriteCommentText() {
    const { isLogin , setIsLogin } = useLoginContext();
  return (
    <form className="mb-6">
        <div className="py-2 px-4 mb-4 bg-white rounded-lg rounded-t-lg border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
            <label htmlFor="comment" className="sr-only">Your comment</label>
            <textarea id="comment" rows={6}
                className="px-0 w-full text-sm text-gray-900 border-0 focus:ring-0 focus:outline-none dark:text-white dark:placeholder-gray-400 dark:bg-gray-800"
                placeholder={isLogin ? "댓글 작성":"로그인 후 댓글 작성이 가능합니다."} required>

            </textarea>
        </div>
        <button type="submit"
            className="inline-flex items-center py-2.5 px-4 text-xs font-medium text-center text-white bg-primary-700 rounded-lg focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900 hover:bg-primary-800">
            댓글
        </button>
    </form>
  )
}

export default WriteCommentText

이 코드는 전역값으로 로그인 여부에따라서 보이는 글을 다르게 설정해주고있다.

 

CommentTop.tsx

import React from 'react'
import CommentNameAndDate from './CommentNameAndDate'
import CommentDropDownButton from './CommentDropDownButton'
import CommentMainText from './commentMainText'

function CommentTop() {
  return (
    <article className="p-6 text-base bg-white rounded-lg dark:bg-gray-900">
        <footer className="flex justify-between items-center mb-2">
            <CommentNameAndDate></CommentNameAndDate>
            <CommentDropDownButton></CommentDropDownButton>
        </footer>
        <CommentMainText></CommentMainText>
    </article>
  )
}

export default CommentTop

이 부분이 댓글을 보여주는 부분의 컴포넌트 이다.

 

CommentNameAndDate.tsx

import React from 'react'

function CommentNameAndDate() {
  return (
    <div className="flex items-center">
        <p className="inline-flex items-center mr-3 text-sm text-gray-900 dark:text-white font-semibold">

            Michael Gough
        </p>
        <p className="text-sm text-gray-600 dark:text-gray-400">
            <time dateTime="2022-02-08" title="February 8th, 2022">
            Feb. 8, 2022
            </time>
        </p>
	</div>
  )
}

export default CommentNameAndDate

 

CommentDropDownButton

import React from 'react'

function CommentDropDownButton() {
  return (
    <>
    <button id="dropdownComment1Button" data-dropdown-toggle="dropdownComment1"
        className="inline-flex items-center p-2 text-sm font-medium text-center text-gray-500 dark:text-gray-400 bg-white rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
        type="button">
        <svg className="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 3">
            <path d="M2 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm6.041 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM14 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z"/>
        </svg>
        <span className="sr-only">Comment settings</span>
    </button>
    <div id="dropdownComment1"
        className="hidden z-10 w-36 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600">
        <ul className="py-1 text-sm text-gray-700 dark:text-gray-200"
            aria-labelledby="dropdownMenuIconHorizontalButton">
            <li>
                <a href="#"
                    className="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Edit</a>
            </li>
            <li>
                <a href="#"
                    className="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Remove</a>
            </li>
            <li>
                <a href="#"
                    className="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Report</a>
            </li>
        </ul>
    </div>
    </>
  )
}

export default CommentDropDownButton

 

CommentMainText.tsx

import React from 'react'

function CommentMainText() {
  return (
    <>
        <p className="text-gray-500 dark:text-gray-400">Very straight-to-point article. Really worth time reading. Thank you! But tools are just the
            instruments for the UX designers. The knowledge of the design tools are as important as the
            creation of the design strategy.</p>
        <div className="flex items-center mt-4 space-x-4">
            <button type="button"
                className="flex items-center text-sm text-gray-500 hover:underline dark:text-gray-400 font-medium">
                <svg className="mr-1.5 w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 18">
                    <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5h5M5 8h2m6-3h2m-5 3h6m2-7H2a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3v5l5-5h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1Z"/>
                </svg>
                Reply
            </button>
        </div> 
    </>
  )
}

export default CommentMainText

 

이렇게 컴포넌트로 댓글을 만들수있게 분리해두었다. 아직은 서버를 구현하지않아서 텍스트로 넣어놓은 닉네임이나 날짜 실제 댓글 데이터는 이후에 commentBox에서 받아서 props로 각 컴포넌트에 넘겨줄 계획이다.

 

 

반응형
반응형

트러블

지금만들고있는 프로젝트에서 특정 API는 USER권한을 가진 계정만 접근이 가능하게 세팅을 해두었다. 

그런데 분명히USER 권한을 가진계정으로 로그인을 하고 SecurityContextHolder에 "USER" 가 저장되는것 까지 디버그를 찍어서 확인해보는데도 해당 API로 접근하지않고 권한이없어서 login으로 리디렉션 되는 문제가있었다. 

 

어제 이 문제로 몇시간을 붙잡고있었는데 자고일어나서 다시 해보니 해결방법이 쉽게 나왔다. 

(이래서 휴식이 중요한듯하다 안되는거붙잡고있어도 안되는건 어쩔수가 없나보다.)

 

원인

SpringSecurity에서 역할을 검사할때 내부적으로 ROLE_ 접두사를 사용한다고 한다. 

그래서 정확한 문제가 뭐였냐 하면 내가보내는 역활은 USER 였고 

Security가 검사하는 역활은 ROLE_USER 였다.. (허무하다)

 

해결

그래서 Security에서 역활을 불러오는 메서드인 getAuthority( ) 에 "ROLE_" + role 로 접두사로 ROLE_ 을 붙이게 해줬더니 해결이 되었다...

 

한줄요약

 

SpringSecurity Role 검사는 ROLE_역할이름 으로 검사한다. 혹시 역할이 정확하게 들어갔는데 통과되지않는다면 ROLE_ 접두사를 붙였는지 잘 확인해보자.

반응형
반응형

 

트러블

도메인과 서브도메인에 프론트와 백을 호스팅해준후에

로그인이 잘되고 쿠키도 잘 받아올수있었다. 

그런데 이제 로그아웃할때 로그인시 받았던 쿠키를 삭제해주는 함수가 먹히지않았다. 

deleteCookie('Authorization');

 

cookies-next 라이브러리의 deleteCookie 함수인데 로컬에서 할때는 쿠키이름만 넣고도 잘 지워져서 호스팅후에도 문제없이 잘동작할줄 알았는데 로그아웃을 아무리 눌러도 쿠키가 지워지지않았다. 

 

원인

찾아보니 쿠키를 삭제할때 서버에서 받은쿠키의경우 domain 이나 path 같은 여러 세팅들도 넣어줘야 삭제가 된다는것을 알았다. 

 

해결

변경후 쿠키삭제 코드

 const deleteCookie = (name:string) => {
      setCookie(name, '', { 
        domain: '.soolae-server.shop', 
        path: '/', 
        maxAge: -1 
      });
    };
    
    
    deleteCookie('Authorization');

쿠키를 만료시간으로 세팅시켜서 쿠키를 없애주는 deleteCookie함수를 새로 만들어주었다. 

가운데 ' ' 이 공백으로 쿠키의 내용을 지우고 세팅을 도메인으로 설정해주면서 maxAge를 -1로 바꿔서 즉시 만료되도록해서 쿠키를 지워주게 된다.

 

요약 

서버에서 받은 쿠키는 도메인이나 path 의 설정이있어서 deleteCookie같은 메서드로 지워지지않는다.

지우기위해서 옵션을 넣어주거나 해당쿠키의 만료시간을 변경시키는방법으로 지워줄수있다.

반응형
반응형

트러블

칵테일 프로젝트를 진행하면서 로컬에서는 쿠키를 서로 잘 주고받았는데 호스팅을하고나니

서버에서 보낸쿠키를 vercel호스팅한 페이지에서 받을수없는 문제가 발생했다.

쿠키는 로그인에 필요해서 필수적으로 해결이 되어야한다. 

 

원인

찾아보니 서로 다른 도메인끼리는 쿠키교환이 안된다고한다.

그래서 필요한게 서브도메인이라는 개념인데

naver.com 이 도메인이라면

blog.naver.com 은 서브도메인이되는개념으로

이런 도메인구조에서는 서로 같은 도메인으로 인식되고 실제로 하나의 도메인만 사용하기때문에

도메인도 하나만있어도되고 쿠키교환도 가능하다는 점을 알게되었다.

 

www.soolae-server.shop 에 vercel 이연결되어서 서브도메인에 vercel을 연결해주었다.

 

Sool lae

1. 하이볼 잔에 얼음을 채운다. 2. 보드카 40ml, 복숭아 리큐르 20ml, 오렌지 주스 40ml, 크랜베리 주스 40ml를 붓는다. 3. 살살 저어준 뒤 오렌지 슬라이스로 장식한다.

www.soolae-server.shop

 

Vercel에 도메인 연결하기

 

vercel 에 호스팅한 프로젝트로 들어가서 settings - Domains 에들어가준다.

돋보기가 있는부분에 연결할 도메인을 입력하고 Add를 눌러준다.
아마 정상적으로 호스팅되는 도메인을 입력하면 선택하는게 3개나올텐데

제일 아래쪽에 있는걸 선택하면 현재입력한 도메인만 생성해준다.

제일아래 Add 도메인 을 선택하고 Add

나는 www.soolae-server.shop 만 이용할 계획이므로 제일 아래를 선택한다 다른 옵션들은 서브 도메인에서 도메인으로 리다이렉트시키거나 도메인에서 서브도메인으로 리다이렉트 시켜주는 옵션인것같다. 

 

도메인을 추가해주고나면 빨간색으로 Invalid Configuration이라고 뜰텐데

Value에 나와있는 ipv4 주소를 Route53에 추가해놓은 도메인으로가서 레코드추가해주면 끝이다. 

 

Route53의 호스팅영역 생성방법은 이 포스팅을 참고

 

(트러블 슈팅) - https 요청에러

프로젝트를 서버에올리고 진행하면서 문제가 생겼다.  get 요청으로 데이터는 잘받아와지는데 post요청으로 동작하는 로그인이 동작하지않았다 .  우선 내 프로젝트환경은 클라이언트부분은 n

colazoa.tistory.com

 

레코드 이름에 서브도메인이랑 똑같이 www 를 붙여주고 레코드 유형은 A 

값에는 복사한 Value인 ipv4 주소를 넣어주고 레코드를 생성해주면 Route53에서 해줘야하는일은 끝난다.

 

이제 다시 vercel로 돌아와서 확인해보면 

이렇게 나오면 도메인이 잘 등록된것이다. 

 

서브도메인에 클라이언트부분을 호스팅하면서 본래 도메인에 호스팅해놓은 api를 다시 옮기지않아도 되고 인증서도 그대로 쓸수있게되어서 예상했던것보다 시간을 많이 단축할수있었다.

도메인이 같아지면서 서버에서 쿠키를 만드는 코드도 수정해주었다.

Java

ResponseCookie cookie = ResponseCookie.from("Authorization",token)
        .path("/")
        .maxAge(60*60*1000)
        .secure(true)
        .domain(".soolae-server.shop")
        .sameSite("None")
        .build();
response.addHeader("Set-Cookie", cookie.toString());

이렇게 코드를 수정하면서 호스팅해놓은 페이지에서도 로그인시에 쿠키를 잘받아올수있게되었다. 

 

그러나 추가적인 문제가 발생했는데. 이제는 쿠키가 지워지지않는다..

 

 

쿠키가 안지워지는경우(트러블 슈팅)

도메인과 서브도메인에 프론트와 백을 호스팅해준후에로그인이 잘되고 쿠키도 잘 받아올수있었다. 그런데 이제 로그아웃할때 로그인시 받았던 쿠키를 삭제해주는 함수가 먹히지않았다. delete

colazoa.tistory.com

 

이렇게 해결해 주었다. 

 

이제 로그인도 잘되고 큰 에러없이 프로젝트가 잘돌아가는것같다.

반응형
반응형

예전에 학원을다니면서 팀플로 만들었던 프로젝트가있는데

한동안 신경을 안썻더니 aws 에 올려놨던 데이터베이스가 계정이 폭파되면서 함께 없어졌다.. (이메일 체크를잘하자..)

다시 살려보려고 백엔드 부분 코드를 봤더니 굉장히 단순하고 왜 이렇게 했을까 싶은 부분들이 많았다.

그래서 Java로 다시 만들고 만드는김에 필요한 기능들을 조금 더 추가 해보고 DB 구조도 다시 만들어보기로 했다.

그렇게 오래 걸릴것같진않은데 클라이언트부분도 너무 예전에 만든코드라서 읽어보면서 하려면 머리는 조금 아플것같다. 

 

node.js 서버코드 

//common js 구문 import ---> require("모듈")
//express

const express = require("express");
const cors = require("cors");
const multer = require("multer")
const mysql = require("mysql");
const bcrypt = require('bcrypt'); //암호화 API
const saltRounds = 10;

//서버 생성
const app = express();

//포트번호
const port = 8080;

//브라우져의 cors이슈를 막기 위해 설정
app.use(cors());

// json형식 데이터를 처리하도록 설정
app.use(express.json());
// upload폴더 클라이언트에서 접근 가능하도록 설정
app.use("/upload",express.static("upload"));
//storage생성
const storage = multer.diskStorage({
    destination: (req,file,cb)=>{
        cb(null,'upload/')
    },
    filename:(req,file,cb)=>{
        const newFilename = file.originalname
        cb(null,newFilename)
    }
})
//upload객체 생성하기
const upload = multer({ storage : storage });
//upload경로로 post 요청시 응답 구현하기
app.post("/upload",upload.single("file"),async (req,res)=>{
    res.send({
        imageURL:req.file.filename
    })
})

//mysql 연결 생성
const conn = mysql.createConnection({
    host:process.env.AWS_ACCESS_HOST,
    user:"admin",
    password:process.env.AWS_ACCESS_KEY,
    port:"3306",
    database:"TeamProject"
})
conn.connect();

// conn.query("쿼리문","콜백함수")

app.get('/place',(req,res)=>{
    conn.query("select * from City",(error,result,field)=>{
        if(error){
            res.send(error)
        }else{
            res.send(result)
        }
    })
})
//http://localhost:8080/special/1
//req{ params: {no:1}}
// 나라정보 받아오기
app.get("/place/:place",(req,res)=>{
    const {place} =req.params;
    conn.query(`select * from City where cityname = "${place}"`,(err,result,field)=>{
        if(err){
            res.send(err)
        }else{
            res.send(result)
        }
    })
})

//나라별 마커 포인트 받아오기
app.get("/marker/:place",(req,res)=>{
    const {place} =req.params;
    conn.query(`select * from SpotPlace where Nation = "${place}"`,(err,result,field)=>{
        if(err){
            res.send(err)
        }else{
            res.send(result)
        }
    })
})

//회원가입요청
app.post("/join",async (req,res)=>{
    //입력받은 비밀번호를 mytextpass로 저장
    const mytextpass = req.body.m_pass;
    let myPass = ""
    const {m_name,m_nickname,m_email} = req.body;

    // 빈문자열이 아니고 undefined가 아닐때
    if(mytextpass != '' && mytextpass != undefined){
        bcrypt.genSalt(saltRounds, function(err, salt) {
            //hash메소드 호출되면 인자로 넣어준 비밀번호를 암호화 하여 콜백함수 안 hash로 돌려준다
            bcrypt.hash(mytextpass, salt, function(err, hash) {// hash는 암호화시켜서 리턴되는값.
                // Store hash in your password DB.
                myPass = hash;
                conn.query(`insert into member(m_name,m_pass,m_email,m_nickname) 
                values( '${m_name}' , '${myPass}' , '${m_email}' , '${m_nickname}')`
                ,(err,result,fields)=>{
                    console.log(result);
                    res.send("등록되었습니다.")
                })
            
            });
        });
    }
    console.log(req.body)
})
//로그인 요청 하기 
app.post("/login",async(req,res)=> {
    // 1.) useremail 값에 일치하는 데이터가 있는지 확인한다.
    // 2.) userpass 암호화해서 쿼리 결과의 패스워드랑 일치하는지 체크
    //{"useremail":"ㅁㄴㅇ""userpass":"asd"}
    const {useremail,userpass }=req.body;
    conn.query(`select * from member where m_email = '${useremail}'`, (err,result,fields)=>{
        //결과가 undefined 가 아니고 결과의 0번째가 undefined가 아닐때= 결과가 있을때 
        if(result != undefined && result[0] !=undefined ){
            bcrypt.compare(userpass,result[0].m_pass, function(err,rese){
                //result == true
                if(result){
                    console.log("로그인 성공");
                    res.send(result);
                }else{
                    console.log("로그인 실패");
                    res.send(result);
                }
            })
        }else{
            console.log("데이터가 존재하지 않습니다.");
        }
    })
})

//비밀번호찾기 
app.post("/findPass", async (req,res) => {
    const {useremail} = req.body;
    console.log(req.body)
    conn.query(`select * from member where m_email = '${useremail}'`,(err,result,field)=>{
        if(result){
            console.log(`결과${result[0].useremail}`)
            res.send(result[0].m_email)
        }else{
            console.log(err)
        }
    })
})

//패스워드 변경 요청
app.patch("/updatePass",async (req, res)=>{
    console.log(req.body)
    const {m_pass,m_email} = req.body;
    //update 테이블이름 set 필드이름= 데이터값 where 조건
        const mytextpass = m_pass;
    let myPass = ""

    if(mytextpass != '' && mytextpass != undefined){
        bcrypt.genSalt(saltRounds, function(err, salt) {
            //hash메소드 호출되면 인자로 넣어준 비밀번호를 암호화 하여 콜백함수 안 hash로 돌려준다
            bcrypt.hash(mytextpass, salt, function(err, hash) {// hash는 암호화시켜서 리턴되는값.
                // Store hash in your password DB.
                myPass = hash;
                conn.query(`update member set m_pass='${myPass}' where m_email='${m_email}'`
                ,(err,result,fields)=>{
                    if(result){
                        res.send("등록되었습니다.")
                    }
                    console.log(err)
                })
            
            });
        });
    }
})

//추천 관광지 받아오기
app.get("/recommend/:place",(req,res)=>{
    const {place} =req.params;
    conn.query(`select * from SpotPlace where Nation = "${place}" and recommend IS NOT NULL order by recommend`,(err,result,field)=>{
        if(err){
            res.send(err)
            console.log(err)
        }else{
            res.send(result)
            console.log(result)
        }
    })
})

app.get("/citydesc/:place",(req,res)=>{ //axios.get('/citydesc/${places}')
    const {place} = req.params;
    conn.query(`select month_1,month_2,month_3,month_4,month_5,month_6,month_7,month_8,month_9,month_10,month_11,month_12 from City where cityname = '${place}'`,
    (err,result,fields)=>{
        if(result){
            res.send(result)
            console.log(result)
        }
        console.log(err)
    })
})





app.listen(port,()=>{
    console.log("서버가 구동중입니다.")
})

 

이제 이 코드를 베이스로 다시 Db 구조를 짜고 Spring 으로 api 를 만들어 주자

쿼리를 왜 저렇게 만들었나 싶은데 * 로 모든 필드를 가져오도록 써놔서

테이블에 어떤 필드들이있었는지 알수없는게 몇개가 있다.

테이블 이름이랑 기능이 매치가 되지않는것도 몇개 있기도해서 테이블은 대대적인 재공사가 필요해 보인다..

 

유저를 정보를 이용해야하는 기능이 만들어져있지않아서 로그인기능에 필요한 유저테이블은 빼버렸다. 

추후에 로그인기능을 추가하고 유저정보를 이용하는 기능을 만들게 되면 필요해지겠지만

지금 현재의 기능에서는 이 테이블들만 이용된다.

 

관광지의 도시들을 추가하게되고 

각 도시들이 관광스팟을 여러개 가지고있게된다 

도시의 월별로 관광정보가 담겨있고 

도시를 기준으로 포스팅 url 도 담겨있게된다. 

 

city를 부모 테이블로해서 자식 테이블이 여러개 연결되어있다. = CityEntity 에 oneToMany로 연결해서 다 가져올수있다.

@Entity
@Table(name = "city")
public class CityEntity extends BaseEntity {
    private String name;
    private String coordinate;
    @OneToMany(mappedBy = "city", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<SpotPlaceEntity> spotPlaceList;

    @OneToMany(mappedBy = "city", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<CityMonthlyInfoEntity> cityMonthlyInfoEntities;

    @OneToMany(mappedBy = "city", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<RecommendPostsEntity> recommendPostsEntities;

    public Double getLat(){
        String[] split = coordinate.split(",");
        return Double.parseDouble(split[0]);
    }

    public Double getLon(){
        String[] split = coordinate.split(",");
        return Double.parseDouble(split[1]);
    }
}

 

CityEntity에 자식테이블들을 연결해서 모두 조회할수있도록 해주었다. 

프론트엔드에서 도시를 선택하고 들어가면 그 도시에 관련된정보들을 한페이지에서 모두 조회하기때문에.
한 엔티티로 모든 데이터를 다 조회할수있게 넘겨주는게 좋을것같았다.

 

나머지 엔티티들도 db구조에 맞춰서 구현해주고 컨트롤러를 만들어 주었다.

@RestController
@RequestMapping("place")
@RequiredArgsConstructor
public class CityController {
    private final CityBusiness cityBusiness;
    // 나라정보 받아오기 /place/:place
    @GetMapping("{placeName}")
    public Api<CityResponse> getPlace(@PathVariable("placeName") String placeName) {
        CityResponse city = cityBusiness.findCity(placeName);
        return Api.OK(city);
    }

    // 추천 관광지 받아오기 "/recommend/:place"
    @GetMapping("spot/{placeName}")
    public Api<List<SpotResponse>> getSpotPlaces(@PathVariable("placeName") String placeName) {
        List<SpotResponse> city = cityBusiness.findSpotList(placeName);
        return Api.OK(city);
    }

    // 나라의 월별정보
    @GetMapping("month/{placeName}")
    public Api<List<MonthInfoResponse>> getMonthInfo(@PathVariable String placeName){
        List<MonthInfoResponse> monthInfo = cityBusiness.findMonthInfo(placeName);
        return Api.OK(monthInfo);
    }
}

 

각 정보별로 메서드를 만들어줬다 도시이름을 파라미터로 받아서 해당도시를 db에서 조회하고 

도시에 관련된 각각의 정보들을 response로 컨버팅해서 내보내주게 된다.

 

 

여행추천 서비스 더 나들이 | Notion

사용한 기술 : React.js, Redux, SpringBoot3, MySql, GoogleMaps API, OpenWeatherMap API

humane-cut-ba5.notion.site

 

이렇게 프로젝트를 다시 살려보았다. 

그때 당시는 DB의 테이블에관한 개념도 별로없었고 테이블구조를어떻게 만들어야할지도 잘 몰랐어서

한 테이블에 1월 ~ 12월까지 필드를 만들어놓고 각 필드에 월 정보를 다 넣는식으로 만들고

city 테이블에 spot데이터도 모두 같이 들어있었던것같다.

다시 만들면서 보니까 그동안 공부한시간들이 조금 더 나은 구조를 만들수있게 도와준것같아서 기분이 좋았다.

반응형
반응형

트러블

프로젝트를 서버에올리고 진행하면서 문제가 생겼다. 

 

get 요청으로 데이터는 잘받아와지는데 post요청으로 동작하는 로그인이 동작하지않았다 . 

 

우선 내 프로젝트환경은

클라이언트부분은 next 로 만들어서 vercel 에 호스팅해주었고

api 부분은 aws 의 ec2에 호스팅해 두었다.

그래서 처음엔 cors에러인가 했는데 서버부분에 cors 처리를 이미 다 해두어서 cors 에러는 아니였다. 

 

원인

그래서 찾아보니

vercel로 호스팅한 페이지는 https로 되어있고 서버는 http로 호스팅이되어서 이런 에러가 발생한다는걸 발견했다. 

Mixed Content: The page at 'https://...' was loaded over HTTPS...

 

이걸 해결하기위해서 프론트에서 메타데이터에

어떤 태그를 추가해주면 해결된다고해서 추가해줘봤지만 실패했다. 

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"></meta>

 

 

결국 서버인 스프링을 https로 배포해야된다고 해서

스프링을 https로 띄우기위해서 상당히 많은 시도를 해봤다. 

그러나 가장간단한방법으로 해결했다. 

 

해결

우선은 서버에 올려야하기때문에 도메인을 구매해주었다. (도메인은 가비아에서 구매할수있다.)

 

웹을 넘어 클라우드로. 가비아

그룹웨어부터 멀티클라우드까지 하나의 클라우드 허브

www.gabia.com

 

구매한 도메인은 aws에서 사용하기위해 route53 에서 등록해주었다.

 

호스팅영역을 생성하고 레코드 생성을 눌러서 레코드를 만들어준다.

 

레코드이름부분과 값부분에 도메인을 사용할 ec2인스턴스의 public ip를 넣어주면

해당 도메인주소로 접근할때 ec2로 연결시켜준다.

 

네임서버 설정

레코드유형에 ns라고되어있는 레코드가있고 값에 네임서버 주소들이있다. 

이 주소를 도메인을 구매한곳에서 네임서버 등록을 해줘야한다. 

 

만약 나처럼 가비아에서 구매했다면 가비아에 들어가서 구매한도메인을 클릭하고들어가면

네임서버옆에 설정버튼이 있을텐데 이걸 클릭하고 들어가서 .

1 2 3 4차에 route53에 있는 네임서버 주소값을 넣어주면된다 

* 입력할때 route53에는 마침표가들어있으니 마침표는 다 빼주어야한다. 

 

이렇게 네임서버 등록까지 완료하면 이제 인증서를 받을 준비가 끝났다. 

 

SSL 발급

 

나는 certbot 을 이용해서 ssl 인증서를 발급받는 방법을 사용했다.

 

ec-2 terminal

sudo yum install certbot
sudo certbot certonly --standalone -d 구매한도메인.com

 

이렇게 명령어를 실행하면 /etc/letsencrypt/live 경로에 입력한도메인폴더가 생기고

해당폴더안에 

fullchain.pem 

privkey.pem 가 발급되어있다. 

이제 이파일들을 스프링이 읽을수있는 파일인 PKCS12 형식으로 변경해주어야한다. 

sudo openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem 
-out keystore.p12 -name ttp -CAfile chain.pem -caname root

 

이 명령어를 입력하면 keystore.p12 라는 파일이 생긴다. 

이 명령어를 입력하면 해당 키의 비밀번호를 입력하게되는데 이 파일을 사용하기 위해서 꼭 필요하니

어딘가에 메모해두자. 

 

HTTPS로 run 시키기 

실행시킬 스프링 프로젝트의 resouces에 ssl 폴더를 만들어주고 이 안에 keystore.p12 파일을 넣어준다.

 

properties 세팅 .

# application-aws.yml
server:
  port: 443  # aws 환경에서 사용할 포트
  ssl:
    key-store: classpath:ssl/keystore.p12
    key-store-type: PKCS12
    key-store-password: ${P12_KEY}
    enabled: true

 

나는 yml이 편해서 yml로 만들어주었다 .  
이렇게 세팅을 마치고 빌드한후에 실행시켜주면 

도메인으로 접근하고 api 요청에대한응답도 잘 받아오는걸 확인할수있다. 

 

https 가 중요하다고하지만 포트폴리오 단계에서적용해볼 생각은 한번도 해본적없었는데 우연히 https에서 http로 요청하면 에러가 발생한다는걸 알게되고 멀리 돌아왔지만 스프링을 https로 호스팅하는 방법도 알게되어서 굉장히 기쁘다. 

 

이번에 찾아보면서 nginx에 대해서도 많이 보고 aws에서도 로드밸런서나 route53에 대한부분을 많이 봤는데 nginx 는 추후에 따로 더 찾아보면서 프로젝트에 적용해보고싶어졌다. 

스프링에서는 자체적으로 tomcat을 가지고있어서 nginx같은 웹서버가 없이도 웹을 동작시킬수있지만 나중에 서버하나로는 부하가 심한경우에 nginx를 제일앞단에 사용하고 서버를 여러개 붙여서 로드밸런싱이란걸 해줄수있다는점이 상당히 흥미로웠다. 물론 지금단계에서는 웹에올려놔도 나밖에 안보지만 ... 부하가 발생한 후에 적용하면 늦는다는 생각으로 포폴이 어느정도 완성되면 공부해서 적용하고싶다.

 

 

SpringBoot에 SSL 인증서를 적용해보자 (feat. AWS EC2)

SSL, HTTPS, SpringBoot

velog.io

이 글을 보고 에러를 해결한후에 작성하는 트러블 슈팅이다. 혹시 부족하거나 잘 모르겠는부분이있다면 이 글도 참고하면 좋을것같다.

반응형
반응형

 

트러블

이번엔 프론트엔드에서 발생한 문제였다.

문제해결에 시간이 오래걸리진 않았지만 특이하다고 생각되서 트러블 슈팅에 저장해놓고 기억해두려고한다.

 

이제 서버에 페이지를올리고 어느정도 확인을 하면서 진행중인데. 모바일과 컴퓨터에서 보는화면이 달랐다.

 

크롬웹으로본 반응형 웹페이지.
모바일로 본 반응형 웹 페이지

 

분명히 서버에올라가있는 같은 페이지인데 모바일에서보면 글씨가 하얗게 날아갔다. 

하나씩 비교해보니 휴대폰이 다크모드여서 그런가 싶어서 데스크톱쪽도 다크모드로 변경하고 봤지만 똑같았다.

 

원인

그래서 모바일에서는 다크모드일때 검정색인 부분은 모두 강제로 하얗게 변경한다는 점을 발견했다. 

데스크톱에서는 다크모드일때 적용될 css를 따로 만들어서 넣어주어야하지만 

모바일에서는 별도로 다크모드 css를 적용해주지않아도

검은색은 하얀색으로 하얀색은 검정색을 반전시켜주는 효과가있었다.

그래서 color가 따로 지정되어있지않은 <p> 태그에 있는 검정글자들이 모바일에서 하얗게 변경되어서 안보였던것이다.. 

 

해결

문제는 text-color 를 적용해주면서 간단하게 해결되었다.

text-color 적용후

 

반응형
반응형

트러블

로컬에서 잘돌아가던 타임리프 페이지가 ec2 서버에 올리고 페이지를 못찾아서 에러가 발생했다..


Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/index.html]")] with root cause

org.thymeleaf.exceptions.TemplateInputException: Error resolving template [/common/header], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "common/layout" - line 6, col 11)

 

[THYMELEAF][http-nio-8081-exec-1] Exception processing template "index": An error happened during template parsing (template: "class path resource [templates/index.html]")

org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/index.html]")

 


원인

이렇게 index.html에서 타임리프 레이아웃을 사용해서 헤더를 불러서 붙여놨는데 header를 찾을수없다는 이야기였다.


layouy.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
<script defer th:src="@{/js/bootstrap.bundle.js}"></script>
<th:block th:replace="~{/common/header::header}"></th:block>
<body>
<th:block layout:fragment="content"></th:block>
</body>
</html>

지금 헤더를 찾는부분이 절대경로로 /common/header::header 로 되어있는데 이 부분을 

 

해결

common/header::header 로 상대경로로 수정해주면 작동이잘된다.. 

 

서버에서는 프로젝트의 루트 디렉토리가 달라질수있어서 상대경로로 지정해야한다고한다.

하하하.. 몇 시간 정도해메고 ai한테도 물어봤는데 해결을 못하다가 구글링을 열심히해서 찾아냈다. 

반응형

+ Recent posts