묻고 답해요
158만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결떠먹는 Three.js
수업을 들으면서...
아직 초반부지만 열심히 듣고 있는데요,섹션3에서 각각의 명령어를 설명하시면서 직접 마우스로 움직이면서 하시더라구요강사님 설명을 들으면서 입력해보고는 있는데 그렇게 움직이면서는 확인이 안되어서요....따로 코딩을 더 넣어야 하는건지 아님 제가 놓친 부분이 있는지 해서요
-
미해결Do It! 장고+부트스트랩: 파이썬 웹개발의 정석
로그인 오류
안녕하세요. 강사님.강의를 보고 개인 웹사이트를 만들었는데요.https://mirihomepage.com/로그인이 안됩니다;;;superuser도 있고, 카테고리/google 사이트 등록까지 분명 다 했는데, https인증 받고, 도메인 연결하고 그러는 사이에 뭔가 달라진건가싶습니다...
-
해결됨[2025년 출제기준] 웹디자인기능사 실기시험 완벽 가이드
A2 연습중인데 메뉴 배경색과 서브메뉴 크기, 갤러리 그림 정리에 문제가 있습니다
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>Green복지재단</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="container"> <header> <div class="header-logo"> <a href="#none"><img src="images/header-logo.jpg" alt="헤더로고"></a> </div> <div class="navi"> <ul class="menu"> <li> <a href="#none">재단소개</a> <div class="sub-menu"> <a href="#none">설립취지</a> <a href="#none">연혁</a> <a href="#none">찾아오시는길</a> </div> </li> <li> <a href="#none">후원하기</a> <div class="sub-menu"> <a href="#none">국내후원</a> <a href="#none">국외후원</a> <a href="#none">맞춤후원</a> </div> </li> <li> <a href="#none">자료실</a> <div class="sub-menu"> <a href="#none">서식자료실</a> <a href="#none">사진자료실</a> <a href="#none">후원양식</a> </div> </li> <li> <a href="#none">스토리</a> <div class="sub-menu"> <a href="#none">웹진</a> <a href="#none">보고서</a> <a href="#none">나의 후원</a> </div> </li> </ul> </div> </header> <div class="slide"> <div class="slide-items"> <a href="#none"><img src="images/slide01.jpg" alt="슬라이드"></a> <a href="#none"><img src="images/slide02.jpg" alt="슬라이드"></a> <a href="#none"><img src="images/slide03.jpg" alt="슬라이드"></a> </div> </div> <div class="contents"> <div class="news"> <div class="tab-inner"> <div class="btns"> <a href="#none">공지사항</a> <a href="#none">갤러리</a> </div> <div class="tabs"> <div class="tab1"> <a href="#none"> SMS 발송 모바일 서비스 개선작업 안내입니다.<B>2020.01.09</B></a> <a href="#none">휴대폰 인증 서비스 개선 작업 기간 연장합니다. <B>2020.01.07</B></a> <a href="#none">카드사 부분 무이자 할부 이벤트 2월 3일까지 혜택<B>2019.12.31</B></a> <a href="#none">올앳 시스템 작업 안내<B>2019.12.20</B></a> <a href="#none">휴대폰 결제 시스템 작업이 완료되었습니다.<B>2019.12.20</B></a> </div> <div class="tab2"> <a href="#none"><img src="images/img1.jpg" alt="이미지1"></a> <a href="#none"><img src="images/img2.jpg" alt="이미지2"></a> <a href="#none"><img src="images/img3.jpg" alt="이미지3"></a> </div> </div> </div> </div> <div class="banner"> <a href="#none"><img src="images/banner.jpg" alt="배너"></a> </div> <div class="shortcut"> <a href="#none"><img src="images/shortcut.jpg" alt="쇼컷"></a> </div> </div> <footer> <div class="footer-logo"> <a href="#none"><img src="images/footer-logo.jpg" alt="푸터로고"></a> </div> <div class="copyright"> 법적고지 개인정보취급방침 개인정보처리방침 <br>상호 : 엣지컴퍼니 | 대표자 : 홍길동 | 개인정보관리책임자 : 장길산 차장 <br>사업장주소 : 서울특별시 강남구 테헤란로 123-56 </div> <div class="family-site"> <select name="" id=""> <option value="https://naver.com">네이버</option> <option value="https://daum.net">다음</option> </select> </div> </footer> </div> </div> <div class="modal-layer"> <div class="modal-contents"> <h1>SNS비회원주문하기 종료 안내</h1> <h2> 안녕하세요, JUST 쇼핑몰 MD 홍길동입니다. 안타깝게도 SNS비회원 주문하기 서비스가 한달 뒤 종료될 예정입니다. <br>회원가입없이 SNS계정을 이용해 그동안 제품주문을 하실수 있었는데, 금번 강화된 개인정보보호법 시행령 제 9조 (부칙 3조 3항)에 의거, SNS를 이용한 상품 주문/결제등이 근래에 많은 보안잇슈로 문제가 되고 있음에 다라 KISA의 권고조치의 일환으로 했습니다. 따라서, 한달뒤인 2019.03.10 이후 모든 비회원 고객님들께서는 회원가입으로 전환 후 실명인증이 되어야 하며, 이는 모든 쇼핑몰/오픈마켓등의 전자상거래서비스의 공통된 사항이라는 점을 안내해드립니다.</h2> <a href="#none" class="close-modal"></a> </div> <script src="script/jquery-1.12.4.js"></script> <script src="script/custom.js"></script> </div> </body> </html>@charset "utf-8"; body{ margin: 0%; background-color: #ffffff; color: #333333; } body a{ color: #333333; text-decoration: none; } .container{ margin: auto; width: 1200px; } header{ height: 100px; } .header-logo{ width: 200px; float: left; padding-top: 20px; text-align: center; } .navi{ border: #333333 1px solid; box-sizing: border-box; width: 800px; float: right; } .contents{ height: 200px; display: flex; } .contents>div{ width: 400px; } footer{ height: 100px; display: flex; } .footer-logo{ width: 200px; line-height: 110px; } .copyright{ width: 700px; text-align: center; padding-top: 10px; } .family-site{ width: 200px; padding-top: 30px; padding-left: 60px; } .modal-layer{ display: none; } select{ text-align: center; } /* 메뉴 */ ul.menu{ list-style: none; position: relative; z-index: 10; padding: 0; } ul.menu>li { float: left; text-align: center; width: 25%; } ul.menu>li>a{ padding: 2px; transition: 0.5s; } .sub-menu>a{ display:block; text-align: center; transition: 0.5s; } .sub-menu{ display: none; } .sub-menu a:hover{ color: #fff; background-color: #333333; } .menu li:hover>a{ color: #fff; background-color: #333333; } /* 슬라이드 */ .slide{ position: relative; height: 300px; overflow: hidden; } .slide-items{ font-size: 0; position: absolute; width: 3600px; height: 300px; } /* 뉴스,갤러리 */ .tab-inner{ padding-top: 20px; } .tab1{width: 95%; padding: 7px; margin: auto; box-sizing: border-box; border: #333333 1px solid; display: none;} .tab1 a{ display: block; font-size: smaller; box-sizing: border-box; border-bottom: #333333 1px solid; } .tab1 a:last-child{ border-bottom: none; } .tab1 a b{ font-weight: normal; float: right; } .tab2{ width: 95%; margin: auto; box-sizing: border-box; border: #333333 1px solid; } .tab2 a{ text-align:center; } .tab2 a img{ width: 100px; } .btns a{ display:inline-block; border: #333333 1px solid; border-radius: 5px 5px 0 0; padding: 2px; border-bottom: #ffffff; background-color: #bbb; } .btns{ margin-left: 15px; margin-bottom: -1px; } .btns a:last-child{ margin-left: -4px; } a.active{ background-color: #fff; } // 메뉴 $('ul.menu>li').mouseenter(function(){ $('.sub-menu').stop().slideDown() }) $('ul.menu>li').mouseleave(function(){ $('.sub-menu').stop().slideUp() }) // 슬라이드 setInterval(function(){ $('.slide-items').animate({left:'-1200px'},function(){ $('.slide-items').css({left:0}) $('.slide-items a:first-child').appendTo('.slide-items') }) },3500) // 탭메뉴 $('.btns a:first-child').click(function(){ $(this).addClass('active') $(this).siblings().removeClass('active') $('.tab1').show() $('.tab2').hide() }) $('.btns a:last-child').click(function(){ $(this).addClass('active') $(this).siblings().removeClass('active') $('.tab2').show() $('.tab1').hide() })서브메뉴가 내려올때 엄청 크게 내려오고, 메뉴의 큰 카테고리에 배경색이 끝까지 채워지지 않습니다
-
미해결그리드(Grid) 핵심이론 및 실전 활용
grid-row, grid-column span에 관한 질문
grid-column: 2/ 4와 grid-column: 1 / span 3은 동일한 동작을 한다고 하셨습니다. 그러나 Holly Grail 레이아웃 제작(2)에서 .header { grid-column: 1 / 4; } .menu { grid-row: 2 / 4; } .footer { grid-column: 2 / 4; }위 코드와 .header { grid-column: 1 / span 3; } .menu { grid-row: 2 / span 3; } .footer { grid-column: 2 / span 3; }은 다르게 동작합니다. 왜 같게 동작한다고 알려주셨나요..
-
미해결부트스트랩 5(Bootstrap 5) - 기초부터 웹 프로젝트 만들기
services-col mx-2 my-3
버전의 문제인가 싶어서 강사님 수업자료 다운받아서 CSS만, JS만, HTML만 해봤으나 원인을 찾지못하여 글 남깁니다. mx-2가 먹히지 않는것인지 이미지 사이에 공백이 없네요.
-
해결됨모든 웹 개발자가 봐야 할 단 한 장의 지도
퀴즈 답
URL은 리소스 위치고 IP 주소가 컴퓨터 식별하기 위한 주소 아닌가요?
-
해결됨웹 애니메이션을 위한 GSAP 가이드 Part.01
어몽이를 stage의 100% 로 보낼 수 있는 방법이 궁금해요!
키프레임 wrap around 실습하다가 궁금해서 여쭤봐요!해설해주신 대로 어몽이를 x를 420으로 하면부모인 stage 가로사이즈 500px이 바뀌면 어몽이에 적용한 420도 같이 바뀌어야 해서부모 사이즈를 기준으로 하려면 left나 right 속성을 사용하는 방법 밖에 없을까요?
-
미해결[웹 개발 풀스택 코스] HTML&CSS 기초
비디오와 오디오 강의 자료파일이 어디에 있나요..?
해당 강의에서 사용하시는 비디오, 오디오 파일을 어디에서 다운받을 수 있는 지 찾지 못하여 질문 드립니다.. 해당 관련 질문이 하나 있던데 답변 주신대로 찾아보아도 안보여서 질문하게 되었습니다
-
해결됨[2025년 출제기준] 웹디자인기능사 실기시험 완벽 가이드
A1 레이아웃 연습중인데 overflow: hidden이나 box-sizing: border-box;가 적용되지 않는 것 같습니다
질문 하실 때 어떤 유형인지 말씀해주세요. ex) A1 작업하는데 ???이 안됩니다. <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>레이아웃 가로 고정형</title> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="container"> <header> <div class="header-logo"></div> <div class="navi"></div> </header> <div class="slide"> <div></div> </div> <div class="items"> <div class="news"></div> <div class="banner"></div> <div class="shortcut"></div> </div> <footer> <div class="footer-logo"></div> <div class="copyright"></div> <div class="sns"></div> </footer> <script src="js/jquery-1.12.4.js"></script> <script src="js/custum.js"></script> </div> </body> </html> .container { border: 1px red solid; box-sizing: border-box; width: 1200px; margin: auto; } header{ border: 1px red solid; box-sizing: border-box; height: 100px; } header div { border: 1px red solid; box-sizing: border-box; height: 100px; } .header-logo{ width: 200px; float: left; } .navi{ width: 800px; float: right; } .slide{ border: 1px red solid; box-sizing: border-box; } .slide div{ border: 1px red solid; box-sizing: border-box; height: 300px; } .items{ border: 1px red solid; box-sizing: border-box; overflow: hidden; } .items div{ border: 1px red solid; float: left; height: 200px; } .news{ width: 500px; } .banner{ width: 350px; } .shortcut{ width: 350px; } footer { overflow: hidden; } footer div{ border: 1px red solid; box-sizing: border-box; height: 100px; float: left; } .footer-logo{ width: 200px; } .copyright{ width: 800px; } .sns{ width: 200px; }
-
미해결Sigil(시길)을 이용하여 전자책 만들기
시길에 메모장을 복붙하면 깨집니다
첨부해주신 메모장 파일이 시길에 복붙하면 깨지는데 어떻게 해야할까요?
-
해결됨[CSS&JS Master] - 트렌디한 감정기록 일기장 만들기
반응형 디자인 - rem으로 유지보수 높이기강의에서 질문있습니다.
rem을 폰트 말고 width랑 height에서도 쓰셨는데, vw와 vh와의 차이가 궁금합니다.
-
해결됨부트스트랩을 활용한 반응형 웹제작 [실전편] 부트캠프
수강기간이 무제한이된건가요?
영코디쌤 전강좌가 수강기간이 무제한으로 바뀌어있던데 제학습창엔 수강만료날짜가 표시되어있는데 저도 혜택을 받을수있을런지요?
-
해결됨웹 애니메이션을 위한 GSAP 가이드 Part.02
아래 질문 (토이스토리 title에 있는 button에 링크 거는 법)에 대한 해결방법이 이게 맞을까요? 더 좋은 방법이 있으면 알려주세요
navList.forEach((li, index)=>{ const roma = ['I', 'II', 'III']; const arabic = ['1','2','3'] const linkAddr = ['naver.com','google.com','inflearn.com']; // button 링크 주소 li.addEventListener('click',()=>{ if(!playing) { next = index; if(li.classList.contains('active')) return; for(let i=0;i < navList.length; i++) { navList[i].classList.remove('active'); } li.classList.add('active'); const addr = /*html*/`location.href='http://${linkAddr[next]}'`; // button 속성 값 const tl = gsap.timeline() .add(leave[current].play()) .add(titleLeave.play(),'-=1') .set('.title h1',{text:`toystory ${roma[index]}`}) .set('.title p',{text:`토이스토리 시즌 ${arabic[index]}`},'<') .add(enter[next].play()) .add(titleEnter.play()) tl.eventCallback('onComplete',()=>{ const btnLink = document.querySelector('.title button'); // button 요소 가져오기 btnLink.setAttribute('onclick',addr); // button의 onclick 속성 추가 current = next; playing = false; }) playing = true; } }) }) window.addEventListener('load',()=>{ const tl = gsap.timeline() .add(enter1.play()) // enter 타임라인 실행 .add(titleEnter.play()) tl.eventCallback('onComplete', () => { const btnLink = document.querySelector('.title button'); //처음 실행시 button에 속성값 추가 btnLink.setAttribute('onclick',"location.href='http://naver.com'"); //처음 실행시 button에 속성값 추가 playing = false; }) // page03()[1].play() // leave 타임라인 실행 })
-
해결됨웹 애니메이션을 위한 GSAP 가이드 Part.02
토이스토리 practice에서 "WATCH NOW"버튼에 대한 href 속성값을 변경하는 방법
안녕하세요. 4강 토이스토리 practice에서 강의 중 선생님께서 "WATCH NOW" 버튼의 href?값을 페이지에 따라 변경할 수 있다고 하셨는데요. gsap을 이용해서 변경할 수 있는 방법이 있는 건지요?아님 자바스크립트 함수 안에서 직접 세팅하는 걸까요?
-
미해결부트스트랩 5(Bootstrap 5) - 기초부터 웹 프로젝트 만들기
단축키 질문
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 단축키도 중간에 이야기 해주시면 안될까요? 물론 직접 찾아보면서 하긴하지만 잘 안나오는 경우도 있고 그때마다 영상 멈추고 찾아보는거보다 강사님께서 알려주시면 좋을듯해서요..
-
해결됨프로그래밍 시작하기 : 웹 입문 (Inflearn Original)
공부한 내용을 블로그에 정리해서 올려도되나요?
안녕하세요 프로그래밍 시작하기 : 웹 입문 (Inflearn Original) 강의를 듣고 있는 수강생입니다. 혹시 출처를 남기고 공부한 내용을 요약해서 블로그에 게시해도 될까요?
-
미해결HTML+CSS+JS 포트폴리오 실전 퍼블리싱(시즌1)
input의 포커스되었을때 검정선이 사라지지 않아요
안녕하세요 선생님 visivility: hidden;을하여도 외곽의 검정선이 없어지지 않네요 완성 예제파일도 같은 현상이 나오는데 어떻게 해결하면 좋을까요? <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <title>자손선택자vs자식선택자</title> <link rel="stylesheet" href="/CSS/style.css" /> </head> <body> <form class="login"> <span>Email</span> <input type="email" placeholder="Email Address"> <span>Password</span> <input type="password" placeholder="password"> <p> <label> <input type="checkbox"> Keep me logged in </label> <a href="#none">Forgot Your Password?</a> </p> <button>Log in</button> </form>@import url('https://fonts.googleapis.com/css?family=Noto+Sans+KR:300,400,500,700,900&display=swap'); body{ font-family: 'Noto Sans KR', sans-serif; line-height: 1.5em; margin: 0; font-weight: 300; display: flex; justify-content: center; align-items: center; height: 100vh; color: #222; } a{ text-decoration: none; color: #222; } .login{ width: 800px; background-color: #f5f5f5; border: 1px solid #eee; border-radius: 5px; padding: 25px; box-sizing: border-box; box-shadow: 0 0 25px #00000026; } .login span { font-weight: bold; display: block; margin-top: 20px; } .login input[type=email], .login input[type=password] { width: 100%; padding: 15px; box-sizing: border-box; border: 1px solid #ddd; border-radius: 5px; padding-left: 40px; transition: .3s; } .login input[type=email]:hover, .login input[type=password]:hover { border: 1px solid dodgerblue; box-shadow: 0 0 5px dodgerblue; } .login input[type=email]::placeholder, .login input[type=password]::placeholder{ font-style: italic; } .login input[type=email]:focus::placeholder{ visibility: hidden; } .login input[type=email]{ background:#fff url(/img/icon-email.png) no-repeat center left 10px; } .login input[type=password]{ background:#fff url(/img/icon-lock.png) no-repeat center left 10px; } .login p { overflow: hidden; } .login p label { float: left; cursor: pointer; } .login p a { float: right; } .login p a:hover { text-decoration: underline; } .login button { background-color: #2991b1; color: #f5f5f5; width: 300px; padding: 10px; outline: none; border-radius: 5px; border: none; font-size: 24px; transition: 0.3s; } .login button:hover{ background-color: #237a95; }
-
해결됨React, Node.js, MongoDB로 만드는 나만의 회사 웹사이트: 완벽 가이드
ch5-1 관리자 페이지 IP블랙리스트 기능구현 관련
안녕하세요..아래와 같이 에러가뜨는데요;; code: 'MODULE_NOT_FOUND', requireStack: [ '/Users/sungwon/Desktop/Project/Web/company_website/backend/index.js' ]}Node.js v24.4.0[nodemon] app crashed - waiting for file changes before starting....backend > index.js코드 입니다.require("dotenv").config(); const express = require("express"); const mongoose = require("mongoose"); const cookieParser = require("cookie-parser"); const cors = require("cors"); const app = express(); const PORT = 3000; const userRoutes = require("./routes/user"); app.use(express.json()) app.use(express.urlencoded()) app.use(cookieParser()); app.use(cors({ origin: "*", credentials: true, })); app.use("/api/auth", userRoutes); app.get("/", (req, res) => { res.send("Hello world"); }); app.get("/api/check-ip", (req, res) => { const clientIP = req.ip || req.connection.remoteAddress; const blacklistedIPs = JSON.parse(process.env.IP_BLACKLIST || '[]'); console.log("Client IP:", clientIP); console.log("Blacklisted IPs:", blacklistedIPs); if (blacklistedIPs.includes(clientIP)) { return res.status(403).json({ allowed: false, message: "Access denied - IP is blacklisted" }); } res.json({ allowed: true }); }); mongoose .connect(process.env.MONGO_URI) .then(() => console.log("MongoDB와 연결이 되었습니다.")) .catch((error) => console.log("MongoDB와 연결에 실패했습니다: ", error)); app.listen(PORT, () => { console.log("Server is running"); });
-
미해결비전공자를 위한 진짜 입문 올인원 개발 부트캠프
그랩님, 상품 상세 페이지 에러와 의문점 질문드립니다.
그랩님, 강의 잘 듣고 있습니다.다름이 아니라, 상품 상세 페이지 에러와 의문점이 있어서 어떻게 해결해야 하는지 궁금한 사항이 있어 질문 드리게 되었습니다.일단 src/main/index.js 소스 코드를 첨부합니다.import './index.css'; import axios from "axios"; import React from 'react'; import {Link} from 'react-router-dom'; function MainPage(){ const [products, setProducts]=React.useState([]); React.useEffect( function(){ axios.get("제 mock 서버 주소 넣었습니다/products") .then(function(result){ const products=result.data.products; setProducts(products); }).catch(function(error){ console.error("에러 발생:",error); }); },[]); return ( <div> <div id="header"> <div id="header-area"> <img src="../images/icons/logo.png" /> </div> </div> <div id="body"> <div id="banner"> <img src="../images/banners/banner1.png" /> </div> <h1>판매되는 상품들</h1> <div id="product-list"> { products.map(function(product, index){ return ( <div className="product-card"> <Link className="product-link" to={`/products/${product.id}`}> <div> <img className="product-img" src={product.imageUrl} /> </div> <div className="product-contents"> <span className="product-name">{product.name} </span> <span className="product-price">{product.price}원 </span> <div className="product-seller"> <img className="product-avatar" src="../images/icons/avatar.png" /> <span>{product.seller}</span> </div> </div> </Link> </div> ); }) } </div> </div> <div id="footer"></div> </div> ); } export default MainPage;2.src/product/index.js 소스 첨부합니다.import {useParams} from 'react-router-dom'; import axios from "axios"; import { useEffect, useState } from 'react'; function ProductPage(){ // const params=useParams(); const {id} = useParams(); const [product, setProduct] = useState(null); useEffect(function(){ axios.get('제 mock 서버 주소 넣었습니다/products/${id}' ) .then(function (result) { setProduct(result.data); // console.log(result); }).catch(function(error){ console.error(error); } ); },[]); console.log(product); // console.log(params); return <h1>상품 상세 페이지 {id} 상품</h1>; } export default ProductPage;-->여기서부터 의문점과 문제점이 발생하게 되니 읽어주시고 해결할 수 있는 방법을 알려주시면 좋겠습니다. 위 소스에서axios.get('제 mock 서버 주소 넣었습니다/products/${id}') 처럼 소스를 달러 중괄호 아이디 입력하면, 아래와 같은 첨부 사진처럼 에러가 납니다. --> 위 에러 첨부 사진은 어떻게 해결해야 할까요?3. 하지만, 위 소스대로 입력 안하면 axios.get('제 mock 서버 주소 넣었습니다/products/1') 하면 제대로 데이터를 오류 없이 아래 첨부 사진처럼 받아 오는 것을 알 수 있습니다. 3-1. 그랩님, 강의 소스에서 처럼 axios.get('제 mock 서버 주소 넣었습니다/products/${id}')해서 하면 위 첨부 사진 처럼 에러가 나는데요, 성공적으로 오류 없이 불러오고 싶은데요, 어떻게 해야 하나요? 단계별로 어떻게 소스를 수정해야하는지 알려주시면 좋겠습니다.확인하시면 답변 부탁 드립니다.
-
해결됨React, Node.js, MongoDB로 만드는 나만의 회사 웹사이트: 완벽 가이드
ch4-6 관리자 계정 로그아웃 , 삭제 관련
7:27 시점에서, 터미널에서 선생님은,,eyJhbGcioiJOUzI1N,......주소명이 뜹니다만,,,저의 경우, 아래와 같이 몽고DB에 연결이 되었습니다만 뜹니다.... 이 경우 어떻게 해야할가요... [nodemon] starting node index.js[dotenv@17.2.1] injecting env (2) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommitServer is runningMongoDB와 연결이 되었습니다.[nodemon] restarting due to changes...[nodemon] starting node index.js[nodemon] restarting due to changes...[nodemon] starting node index.js[dotenv@17.2.1] injecting env (2) from .env -- tip: ⚙ suppress all logs with { quiet: true }Server is runningMongoDB와 연결이 되었습니다. 아래는 routes폴더에 있는 user.jsconst express = require("express"); const router = express.Router(); const bcrypt = require("bcrypt"); const User = require("../models/User"); const axios = require("axios"); const jwt = require("jsonwebtoken"); router.post("/signup", async (req, res) => { try { const { username, password } = req.body; const existingUser = await User.findOne({ username }); if (existingUser) { return res.status(400).json({ message: "이미 존재하는 사용자입니다." }); } const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, password: hashedPassword, }); await user.save(); res.status(201).json({ message: "회원가입이 완료되었습니다." }); } catch (error) { res.status(500).json({ message: "서버 오류가 발생했습니다." }); console.log(error); } }); router.post("/login", async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ username }).select("+password"); if(!user) { return res.status("401").json({message: "사용자를 찾을 수 없습니다."}); } if(!user.isActive){ return res .status(401) .json({ message: "비활성화된 계정입니다. 관리자에게 문의 주세요."}); } if(user.isLoggedIn){ return res .status(401) .json({message: "이미 다른 기기에서 로그인되어 있습니다."}); } const isValidPassword = await bcrypt.compare(password, user.password); if(!isValidPassword){ user.failedLoginAttempts += 1; user.lastLoginAttempt = new Date(); if(user.failedLoginAttempts >= 5){ user.isActive = false; await user.save(); return res.status(401).json({ message: "비밀번호를 5회이상 틀려 계정이 비활성화되었습니다.", }); } await user.save(); return res.status(401).json({ message: "비밀번호가 일치하지 않습니다.", remainingAttempts: 5 - user.failedLoginAttempts, }); } user.failedLoginAttempts = 0; user.lastLoginAttempt = new Date(); user.isLoggedIn = true; try { const response = await axios.get("https://api.ipify.org?format=json"); const ipAddress = response.data.ip; user.ipAddress = ipAddress; } catch (ipError) { console.error("IP 주소를 가져오는 중 오류 발생:", ipError.message); } await user.save(); const token = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, { expiresIn: "24h" } ); res.cookie("token", token, { httpOnly: true, secure: "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, }); const userWithoutPassword = user.toObject(); delete userWithoutPassword.password; res.json({ user: userWithoutPassword }); } catch (error) { console.error("서버 오류:", error.message); res.status(500).json({ message: "서버 오류가 발생했습니다." }); } }); router.post("/logout", async (req, res) => { try { const token = req.cookies.token; if (!token) { return res.status(400).json({ message: "이미 로그아웃된 상태입니다." }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await User.findById(decoded.userId); if (user) { user.isLoggedIn = false; await user.save(); } } catch (error) { console.log("토큰 검증 오류: ", error.message); } res.clearCookie("token", { httpOnly: true, secure: "production", sameSite: "strict", }); res.json({ message: "로그아웃되었습니다." }); } catch (error) { console.log("로그아웃 오류: ", error.message); res.status(500).json({ message: "서버 오류가 발생했습니다." }); } }); router.delete("/delete/:userId", async (req, res) => { try { const user = await User.findByIdAndDelete(req.params.userId); if (!user) { return res.status(404).json({ message: "사용자를 찾을 수 없습니다." }); } res.json({ message: "사용자가 성공적으로 삭제되었습니다." }); } catch (error) { res.status(500).json({ message: "서버 오류가 발생했습니다." }); } }); module.exports = router; env.에 표기한 부분MONGO_URI=mongodb+srv://sungwon5623:cho121101!@sungwon.oirqw5d.mongodb.net/?retryWrites=true&w=majority&appName=Sungwon JWT_SECRET=c21b6ba5372fa2b8 models폴더에 있는 User.jsconst mongoose = require("mongoose"); const userSchema = new mongoose.Schema( { username: { type: String, required: true, trim: true, minlength: 2, maxlength: 30, }, password: { type: String, required: true, select: false, }, isLoggedIn: { type: Boolean, default: false, }, isActive: { type: Boolean, default: true, }, failedLoginAttempts: { type: Number, default: 0, }, lastLoginAttempt: { type: Date, }, ipAddress: { type: String, trim: true, }, createdAt: { type: Date, default: Date.now, }, }, { timestamps: true, } ); const User = mongoose.model("User", userSchema); module.exports = User;