묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결비전공자를 위한 진짜 입문 올인원 개발 부트캠프
axios error
ngrok 연결도 다 했고 url에다가 POSTMAN으로 GET 요청하면 데이터도 잘 불러와지는데 결과창을 보면 데이터가 불러와지지 않아 화면이 제대로 뜨질 않네요...해결 방법이 있을까요?
-
미해결한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
useContext질문드립니다
선생님 useContext 다른 강의를 보다가 궁금한 점이 생겨서 질문드립니다.우리 수업에서는 context를 위한 파일을 따로 만들지 않고 app.js에 바로 context를 만들었잖아요..?그래서 app.js에 import {createContext} from "react"로 안쓰고 그냥 import React from "react"로 쓴 것인가요?파일을 따로 만들 때랑 아닐 때랑 import를 다르게 하는 것 같아서요..그리고 파일을 따로 만드는 것이 더 일반적인 방식인지 궁금합니다!
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 백엔드 코스
mini project swagger 작성 중 문제가 있어서 질문드립니다.
구현은 끝나서 swagger 만들고 있는데, 계속 swagger가 보내는 json을 못찾는것같습니다. request의 body에도 아무 값이 안나오네요. 도와주세요ㅠㅠAPI 코드app.post("/users", async (req, res) => { const newUser = req.body.newUser; // 여기에서 오류가 납니다. if ((await isAuthPhone(newUser.phone)) === true) { if (checkValidationEmail(newUser.email) === true) { const og = await makeOG(newUser.prefer); console.log(og); const securePersonal = secure(newUser.personal); const user = new UserCollection({ name: newUser.name, email: newUser.email, personal: securePersonal, prefer: newUser.prefer, pwd: newUser.pwd, phone: newUser.phone, og: og, }); await user.save(); await sendWelcomeTemplateToEmail(newUser); console.log( `✅: "${user.name}" 사용자가 신규 가입에 성공했습니다.` ); res.send(user._id); } else { res.status(422).send("NotValidationEmail"); } } else { res.status(422).send("NotAuthPhone"); } });Swagger 코드(yaml 파일은 복붙하니까 이상하게 나와서 이미지로 첨부할게요)(+ 이미지에선 parameter의 name이 body로 되어있지만, 위 API 코드에 맞춰서 name을 newUser로 설정했었지만 같은 에러가 떴었습니다.)에러 메시지Swagger parameter 화면아무리 찾아도 방법을 모르겠어서 올립니다ㅠㅠ.... 도와주세요.....
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
복습 중 질문이 있습니다!
제로초 쌤 강의 잘들으면서 구현하고 있다가 갑자기 머리가 꼬여서 복습 확인 차 질문 올립니다!(reduxjs/toolkit 사용하고 있어서 제로초쌤 강의에서 saga를 바꿔보면서 하고 있습니다)(wsl 문제는 mysql안에서 명령문 익히면서 진행하고 있습니다)컴포넌트에서 어떤 액션을 디스패치 해줄때 안에 dispatch안의 콜백함수의 인자가 액션이 백엔드 주소에 데이터를 요청할때 들어가는 data 값의 형태로 들어가서 그게 백엔드에선 req.body로 들어가고(동적라우팅은 주소에 ${data}로 넣어준 부분이 백엔드의 req.params.(동적라우팅변수)로 들어가는거고) 그 후에 백엔드의 작업이 끝나면 res.json() 안의 인자가 결국 최종적으로 action.payload로 들어오는게 맞나요!?이 문제는 로그아웃하고 난 뒤에 regenertion 문제가 생겨서 소스코드도 보고 로직에 문제가 있나 싶어서 돌려보고 해당 문제로 저랑 같이 질문하신 분의 답글도 봤는데 한번 안보이니까 너무 안보이더라구요. 한번만 봐주시면 감사하겠습니다 로그아웃시 코드도 사진 따로 첨부하겠습니다(그 외 기능은 전부 잘 돌아갑니다! 로그아웃 하고 난 뒤에만 오류가 나요..)항상 질 좋은 강의 답변해주셔서 감사합니다! next.js부분도 화이팅하면서 듣겠습니다~
-
해결됨습관부터 바꿔주는 Node.js & Express 기초
github 권한
안녕하세요 수업듣다가 github권한에 대해서 문의드립니다.저번에 질문남겨주셨을때도 들어간 링크가 404로 떠서private 레포로 되어있는것 같은데, 권한 요청을 드려야하나요?다른 수업자료들도 전부 private으로 되어있는것 같아요!이렇게 1개의 레포만 확인할 수 있습니다..!
-
미해결한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
useRef와 변수의 차이
import "./App.css"; function App() { const count = 0; const increaseCountState = () => { count++; }; return ( <div className="App"> <p>State : {count}</p> <button onClick={increaseCountState}>State 올려</button> </div> ); }안녕하세요 1. useRef 를 공부하고 있는데 리액트의 변수를 관리할 때 state랑 useRef 를 사용하는 것은 알겠습니다.그런데 왜 그냥 일반 변수에서 값을 수정하면 되는데 복잡하게 useRef를 사용할까요? 렌더링 그런것을 떠나서 useRef랑 일반 변수를 사용할 때 차이가 궁금합니다.2. 리액트에서 저 코드가 왜 작동이 안될까요?
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
Server Error Error: The default export is not a React Component in page: "/02-02-counter-state"
강의 들을때마다 점점 지치네요.. 맥 윈도우 다쓰는데 매번 오류나고 뭐가문제인지 모르겠네요 공부를 하려고 해도 의욕이 떨어져요 Server ErrorError: The default export is not a React Component in page: "/02-02-counter-state"This error happened while generating the page. Any console logs will be displayed in the terminal window.Call StackObject.renderToHTMLfile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/render.js (234:19)doRenderfile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (1392:57)<unknown>file:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (1487:34)<unknown>file:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/response-cache.js (63:42)ResponseCache.getfile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/response-cache.js (80:11)DevServer.renderToResponseWithComponentsfile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (1423:53)DevServer.renderToResponsefile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (1559:39)process.processTicksAndRejectionsnode:internal/process/task_queues (95:5)async DevServer.pipefile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (1111:25)async Object.fnfile:///Users/jinho/Desktop/codecamp-frontend-jinho/class/node_modules/next/dist/server/next-server.js (912:21)
-
미해결한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
안녕하세요 질문있습니다.
안녕하세요 페이지 구현 일기쓰기(new)까지 수강하고코딩을 완료하였는데 수정완료 버튼을 누르면 수정이 되질 않는 버그와 console.log(targetDate)를 하면 콘솔에 두번 출력되는 버그가 있어서 깃 링크를 드리고 질문을 하려했으나 깃이 자꾸 오류가나서 그런데 혹시 메일로 파일을 보내드리면 그거에 대한 답변이 가능하신지 궁금하여 여쭤봅니다.
-
미해결습관부터 바꿔주는 Node.js & Express 기초
swagger No operations defined in spec
/swagger.json은 잘 뜨는데/api-docs로 들어가면 'swagger No operations defined in spec!' 이라고 계속 못불러오네요..!수업때 코드 잘 따라친것 같은데, 깃헙에 코드올려주시면 감사하겠습니다..!
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
yarn generate 에러
Invalid Custom Plugin "typescript" 라면서 에러가 뜹니다 어떻게 해결해야 할까요?
-
미해결Node.js에 TypeScript 적용하기(feat. NodeBird)
Sequelize Association 오류
start로 시작한 서버에서는 sequelize가 정상 작동하는데 test를 하면 아래와 같은 오류가 발생합니다 FAIL tests/integration/intergration.test.ts ● Test suite failed to run TypeError: Cannot read properties of undefined (reading 'name') 74 | } 75 | static associate() { > 76 | User.hasMany(Box_1.default, { | ^ 77 | sourceKey: "id", 78 | foreignKey: "UserId" 79 | }); at new HasMany (node_modules/sequelize/src/associations/has-many.js:51:37) at Function.hasMany (node_modules/sequelize/src/associations/mixin.js:34:25) at Function.associate (models/user.js:76:14) at Object.<anonymous> (models/index.js:26:19) at Object.<anonymous> (tests/integration/intergration.test.ts:2:1) Test Suites: 1 failed, 1 total Tests: 0 total Snapshots: 0 total Time: 2.43 s Ran all test suites.sequelize 공식 문서를 보고 그대로 적용해 고쳐보았지만 무엇이 문제인지 잘 모르겠습니다 stackoverflow에서는 sequelize의 오류라고 하는 것 같은데 현재 저의 수준에서는 대화의 내용조차 잘 감이 잡히지 않습니다...공식 문서를 보고 association들을 수정하면 TypeError: Cannot read properties of undefined (reading 'name') 에서 name이 UserId 등으로 바뀐 것도 몇번 보았지만 공식문서에서 나온대로 수정하면 다시 'name'을 읽을때 찾을 수 없다고 나옵니다. 또한at new HasMany (node_modules/sequelize/src/associations/has-many.js:51:37)at Function.hasMany (node_modules/sequelize/src/associations/mixin.js:34:25) 은 들어갈 수 없는 파일처럼 보입니다.import Sequelize, { CreationOptional, InferAttributes, InferCreationAttributes, Model, ForeignKey, } from "sequelize"; import { HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, } from "sequelize/types/associations"; import Bookmark from "./bookmark.js"; import User from "./user.js"; class Box extends Model<InferAttributes<Box>, InferCreationAttributes<Box>> { declare id: CreationOptional<number>; declare box: string; declare img: string; declare createdAt: CreationOptional<Date>; declare updatedAt: CreationOptional<Date>; declare UserId: ForeignKey<User["id"]>; declare getBookmarks: HasManyGetAssociationsMixin<Bookmark>; declare addBookmarks: HasManyAddAssociationMixin<Bookmark, number>; declare hasBookmarks: HasManyHasAssociationMixin<Bookmark, number>; declare countBookmarks: HasManyCountAssociationsMixin; declare createBookmarks: HasManyCreateAssociationMixin<Bookmark>; static initiate(sequelize: Sequelize.Sequelize) { Box.init( { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, box: { type: Sequelize.STRING(15), allowNull: false, }, img: { type: Sequelize.STRING(200), allowNull: true, }, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE, }, { sequelize, timestamps: true, underscored: false, modelName: "Box", tableName: "boxs", paranoid: false, charset: "utf8mb4", collate: "utf8mb4_general_ci", } ); } static associate() { Box.belongsTo(User, { targetKey: "id" }); Box.hasMany(Bookmark, { sourceKey: "id", foreignKey: "BoxId", }); } } export default Box;box.ts입니다import Sequelize, { CreationOptional, InferAttributes, InferCreationAttributes, Model, } from "sequelize"; import { HasManyAddAssociationMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin, } from "sequelize/types/associations"; import Box from "./Box"; class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> { declare id: CreationOptional<number>; // 'CreationOptional' is a special type that marks the field as optional // id can be undefined during creation when using `autoIncrement` declare email: string; declare nick: string; declare password: CreationOptional<string>; declare provider: CreationOptional<string>; declare snsId: CreationOptional<string>; declare createdAt: CreationOptional<Date>; // createdAt can be undefined during creation declare updatedAt: CreationOptional<Date>; // updatedAt can be undefined during creation declare deletedAt: CreationOptional<Date>; // ... declare getBoxs: HasManyGetAssociationsMixin<Box>; declare addBoxs: HasManyAddAssociationMixin<Box, number>; declare hasBoxs: HasManyHasAssociationMixin<Box, number>; declare countBoxs: HasManyCountAssociationsMixin; declare createBoxs: HasManyCreateAssociationMixin<Box>; static initiate(sequelize: Sequelize.Sequelize) { User.init( { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true, }, email: { type: Sequelize.STRING(40), allowNull: true, unique: true, }, nick: { type: Sequelize.STRING(15), allowNull: false, }, password: { type: Sequelize.STRING(100), allowNull: true, }, provider: { type: Sequelize.ENUM("local", "kakao", "github"), allowNull: false, defaultValue: "local", }, snsId: { type: Sequelize.STRING(30), allowNull: true, }, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE, deletedAt: Sequelize.DATE, }, { sequelize, timestamps: true, underscored: false, modelName: "User", tableName: "users", paranoid: true, charset: "utf8", collate: "utf8_general_ci", } ); } static associate() { User.hasMany(Box, { sourceKey: "id", foreignKey: "UserId" }); } } export default User; user.ts입니다
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
쿠키파서 기능
쿠키파서가 없어도 쿠키가 보내지고 데이터를 받을 때 원본이 오는데 쿠키파서 미들웨어가 필수인건가요?
-
미해결MERN STACK 커뮤니티 : 시작부터 배포까지 알려주는 React
빌드하지 않고 테스트가 가능한가요?
'Read(1): 게시글 불러오기' 부분 수강중에 있습니다.강사님께서는 프론트부분 코드를 수정하시고도 npm run build 명령어 입력 없이 즉, 빌드 없이 바로 브라우저에서 테스트가 가능하신 것 같은데 어떻게 하신 건가요?저 같은 경우는 구름 ide라는 클라우드 서비스로 visual studio code를 이용중이며3000번 포트에서는 react를, 5000번 포트에서는 node를 사용중에 있습니다. 그래서 코드 한번 수정 후 테스트 해볼 때터미널에서 client폴더로 이동 -> npm run builid -> server폴더로 이동 -> nodemon index.js이후에 브라우저 들어가서 코드 바꾼 것 잘 적용 되었는지 확인중에 있습니다. 그런데 리액트 코드가 길어지면 길어질수록 빌드하는데 시간이 너무 오래 걸리네요 ㅠㅠ 강사님처럼 빌드하지 않고 바로 확인할 수 있는 방법이 있는지 궁금합니다.추가로 판다코딩님 깃헙 링크좀 부탁드립니다!
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
index.js 오류
post/user/등 강좌 되돌려 보면서 틀린 곳이 있나 확인했는데,어디서 부터 고쳐야 될지 모르겠습니다. index.jsimport React from 'react'; import { useSelector } from 'react-redux'; import PostForm from '../components/PostForm'; import PostCard from '../components/PostCard'; import AppLayout from '../components/AppLayout' const Home = () => { const { isLoggedIn } = useSelector((state) => state.user); const { mainPosts } = useSelector((state) => state.post); return ( <AppLayout> {isLoggedIn && <PostForm/>} {mainPosts.map((post)=><PostCard key={post.id} post={post}/>)} </AppLayout> ); } export default Home;
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
8:24분경 코드 오류나서 수정하셨는데!
8분 20초경 오류나서 코드를 수정하셨는데,case ADD_POST_TO_ME: return { ...state, me: { ...state.me, // 이부분 왜 추가한건가요? Posts: [{ id: action.data }, ...state.me.Posts], }, };코드 주석부분 왜 추가하신건가요..?const dummyUser = (data) => ({ ...data, nickname: "wewewe", id: 1, Posts: [{ id: 1 }], Followings: [{ nickname: "we1" }, { nickname: "we2" }, { nickname: "we3" }], Followers: [{ nickname: "we1" }, { nickname: "we2" }, { nickname: "we3" }], });dummy데이터 구조를 보면Posts를 제외한 nickname, id, Followings 등 변하지 않는 데이터들의 불변성을 유지하기 위해서 추가한것이 맞는건가요?!
-
미해결비전공자를 위한 진짜 입문 올인원 개발 부트캠프
error:03000086:digital envelope routines::initialization error axios오류
안녕하세요 axios 코드 실행 하면 아래 사진과 같이 오류가 뜹니다.
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
ADD_COMMENT_SUCCESS 새로운 post에서만 작동 안 하는 문제
안녕하세요 제로초 님,redux saga immer, faker 까지만 들었구요, 백엔드는 안 만들었고요.faker가 문제가 있다길래,import { faker } from '@faker-js/faker';이거를 대신해서 설치해서 했는데, 이거의 문제는 아닌거 같고요. ADD_COMMENT_SUCCESS가기존의 post에서는 작동을 하는데, 새로운 post에서만 작동 안 하는 문제가 있습니다. 새로운 post.id가 동작을 안 하나 싶어const post = draft.mainPosts.find((post)=> { console.log(post.id == action.data.postId); return post.id == action.data.postId; });를 해봤는데 역시 true가 나옵니다.아래는 브라우저 결과 사진들 입니다.로그인 안 했을 때,로그인 하고 기존의 post에 comment를 달았을 때 -> 정상적으로 동작 함. 로그인 하고 새로운 post을 하나 추가 -> 정상적으로 동작 함. username이 바뀌는 건 faker를 썼고, userId만 그래도 컴포넌트에서 받아서 써서 지금 ADD_POST_TO_ME 동작해서 1에서 2로 바뀌었구요.그러나새로운 post에 comment를 달았을 때 -> 콘솔로그가 true가 나옴에도 동작을 하지 않음. 기존의 post에도 comment가 안 달리는 거면 제가 실수를 한게 분명한데, 새로운 post에만 동작을 안 하는게 이상합니다.ADD_COMMENT_REQUEST를 보냈을 때에도, 갑자기 mainPosts가 초기화 되거나 그런 것도 아니었습니다. 만약 그랬다면 콘솔로그가 false가 나왔어야 합니다. 감사합니다!
-
미해결탄탄한 백엔드 NestJS, 기초부터 심화까지
populate 문제(cats schema 오류) 해결법
강의대로 따라 했으나, 아래와같은 오류 나는경우 해결법 입니다.ERROR [ExceptionsHandler] Schema hasn't been registered for model "comments".Use mongoose.model(name, schema)다른 문의글 보면 답변으로 버전 문제라고 버전을 내리라고 하시는데 , 좀 이상한 답변이라는 생각에진짜 몇시간동안 헤매다가 해결했습니다. 현재기준 최신버전"@nestjs/common": "^9.0.0", "@nestjs/mongoose": "^9.2.1", "mongoose": "^6.9.0",에서 아래와 같이 해결 했습니다. 주석참조.export class CatsRepository { constructor( @InjectModel(Cat.name) private readonly catModel: Model<Cat>, // 해당 라인 추가, 참고로 강의에선 Comments 인데 저는 Cat과 같이 단수형으로 만들어서 Comment 입니다. @InjectModel(Comment.name) private readonly commentModel: Model<Comment>, ) {} async findAll() { const result = await this.catModel .find() // populate 파라미터 변경 .populate({ path: 'comments', model: this.commentModel }); return result; } ... }다른 누군가에게 도움이 되기를
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
VsCode : Import하지 않은 컴포넌트 경고표시X(설정?)
현재 버전 정보들입니다 "next": "^11.1.4", "prop-types": "^15.8.1", "eslint": "^8.33.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0"TypeScript와 React를 사용해서 프로젝트 했을 때는 VsCode에서(웹스톰만의 기능은 아니라고 생각합니다.) import하지 않은 컴포넌트에 대한 경고문이 나와서, 맥북 기준 커맨드+.을 하면 Code Action으로 import을 시켜줄 수 있었습니다.그런데강의를 진행하면서 컴포넌트를 import하지 않은 상황인데도 불구하고, 따로 경고문이 나타나지 않는데, 이게 어떤 설정을 해야하는건지 잘 모르겠습니다.<Menu /> , <Col /> , <UserProfile /> 같은 컴포넌트들입니다.(코드는 이 정도만 첨부하겠습니다.)import PropTypes from "prop-types"; import Link from "next/link"; import { Menu, Input, Row, Col } from "antd"; ... <Row gutter={8}> <Col xs={24} md={6}> {isLoggedIn ? <UserProfile /> : <LoginForm />} </Col> <Col xs={24} md={12}> {children} </Col> <Col xs={24} md={6}> <a href="https://velog.io/" target="_blank" rel="noreferrer noopener">Velog</a> </Col> </Row> </div> ); }; AppLayout.propTypes = { children: PropTypes.node.isRequired, }; export default AppLayout;Import되지 않은 컴포넌트인 <UserProfile />, <LoginForm /> 경고문이 뜨지 않는 사진도 첨부했습니다.
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
수정 페이지 마운트 직후, 온체인지 핸들러가 인풋 값을 ""으로 인식하여, 인풋 값를 지웠을 경우, 온체인지 핸들러가 값의 변동을 인식하지 못하는 문제가 있습니다.
안녕하세요 강의 잘 보고있습니다.수정 페이지 마운트 직후, 온체인지 핸들러가 인풋 값을 ""으로 인식하고, 인풋 값를 지웠을 경우, 온체인지 핸들러가 값의 변동을 인식하지 못하는 문제가 있습니다.즉, 수정 페이지의 각 인풋 디폴트 값들에 데이터가 불러와진 후, 값을 입력하는 것이 아니라 삭제할 경우에 값의 변동을 온체인지 핸들러가 인식하지 못하여 발생합니다.따라서, 인풋의 온체인지 핸들러를 통해 인풋 값들이 비어있는지 확인하는 유효성 검사에 작은 오류가 있습니다. 수정페이지 마운트 직후 데이터를 불러온 뒤, 값이 인풋에 입력된 후, 온체인지핸들러가 변동을 인식하게 할 수 있을까요? 혹시 비슷한 인풋 값들을 객체에 묶어 저장하는 방식을 지양해야 할까요? import { type ChangeEvent, useState, useRef, useEffect } from 'react'; import { useRouter } from 'next/router'; import BoardWrite_presenter from './BoardWrite_presenter'; import { CREATE_BOARD, UPDATE_BOARD, UPLOAD_FILE } from './BoardWrite_queries'; import { useMutation, useQuery } from '@apollo/client'; import { type IBoardWrite_container_Props, type IUpdatedVariables, } from './BoardWrite_types'; import { type Address } from 'react-daum-postcode/lib/loadPostcode'; import { checkFileValidation } from '../../commons/checkFileValidation'; import { Modal, Spin } from 'antd'; import type { RcFile } from 'antd/es/upload'; import type { UploadFile } from 'antd/es/upload/interface'; import { getBase64 } from '@/src/commons/utils/utils'; import { FETCH_BOARD } from '../detail/BoardDetail_queries'; import { type IQuery } from '@/src/commons/types/generated/types'; import { Loading3QuartersOutlined } from '@ant-design/icons'; export default function BoardWrite_container( props: IBoardWrite_container_Props ) { const router = useRouter(); const boardId = router.query.boardId; if (props.isEditing && !boardId) { return ( <Spin indicator={<Loading3QuartersOutlined spin />} /> ) } const { data } = useQuery<Pick<IQuery, 'fetchBoard'>>(FETCH_BOARD, { variables: { boardId, }, }); const fetchBoard = data?.fetchBoard; if (props.isEditing && !boardId && !fetchBoard) { return ( <Spin indicator={<Loading3QuartersOutlined spin />} /> ) } const [createBoard] = useMutation(CREATE_BOARD); const [updateBoard] = useMutation(UPDATE_BOARD); const [images, setImages] = useState('youtube'); const [isOpen, setIsOpen] = useState(false); const [writerError, setWriterError] = useState(false); const [passwordError, setPasswordError] = useState(false); const [titleError, setTitleError] = useState(false); const [contentsError, setContentsError] = useState(false); const [valid, setValid] = useState(false); interface ICoreInput { [key: string]: string writer: string password: string title: string contents: string } const [coreInput, setCoreInput] = useState<ICoreInput>({ writer: fetchBoard?.writer ?? '', password: '', title: fetchBoard?.title ?? '', contents: fetchBoard?.contents ?? '' }) const [coreInputErorr, setCoreInputErorr] = useState({ writer: false, password: false, title: false, contents: false }) useEffect(() => { if (fetchBoard) { setCoreInput({ ...coreInput, writer: String(fetchBoard.writer), title: String(fetchBoard.title), contents: String(fetchBoard.contents), }) } }, [data]) const onChangeCoreInput = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { setCoreInput({ ...coreInput, [e.currentTarget.id]: e.currentTarget.value }); setCoreInputErorr({ ...coreInputErorr, [e.currentTarget.id]: false }); const AllInputs: string[] = []; for (const prop in coreInput) { if (prop !== e.currentTarget.id) { AllInputs.push(coreInput[prop]) } else { AllInputs.push(e.currentTarget.value) } } console.log("AllInputs : " + AllInputs) if (!AllInputs.includes('' && "undefined")) { setValid(true); } else setValid(false); }; const onChangeImages = (e: ChangeEvent<HTMLInputElement>) => { setImages(e.target.value); }; const [input, setInput] = useState({ zipcode: "", address: "", addressDetail: "", youtubeUrl: "" }) const onChangeInput = (e: ChangeEvent<HTMLInputElement>) => { setInput({ ...input, [e.target.id]: e.target.value }); }; const onSubmit = async (e: { preventDefault: () => void }) => { e.preventDefault(); if (coreInput.writer === '') { setWriterError(true); } if (coreInput.password === '') { setPasswordError(true); } if (coreInput.title === '') { setTitleError(true); } if (coreInput.contents === '') { setContentsError(true); } if (coreInput.writer && coreInput.password && coreInput.title && coreInput.contents) { try { const result = await createBoard({ variables: { createBoardInput: { writer: coreInput.writer, contents: coreInput.contents, password: coreInput.password, title: coreInput.title, youtubeUrl: input.youtubeUrl, images, boardAddress: { zipcode: input.zipcode, address: input.address, addressDetail: input.addressDetail, }, }, }, }); void router.push(`/boards/${result.data.createBoard._id}`); } catch (error) { if (error instanceof Error) alert(error.message); } } }; const onUpdate = async (e: { preventDefault: () => void }) => { e.preventDefault(); const updatedVariables: IUpdatedVariables = { boardId, password: coreInput.password, updateBoardInput: { contents: coreInput.contents, title: coreInput.title, youtubeUrl: input.youtubeUrl, images, boardAddress: { zipcode: input.zipcode, address: input.address, addressDetail: input.addressDetail, }, }, }; if (!coreInput.password) { setPasswordError(true); } if (!coreInput.title) { setTitleError(true); } if (!coreInput.contents) { setContentsError(true); } if (!coreInput.password && !coreInput.title && !coreInput.contents) { try { const result = await updateBoard({ variables: updatedVariables, }); void router.push(`/boards/${result.data.updateBoard._id}`); } catch (error) { if (error instanceof Error) alert(error.message); } } }; const onToggleModal = () => { setIsOpen((prev) => !prev); }; const handleComplete = (data: Address) => { let fullAddress = data.address; let extraAddress = ''; if (data.addressType === 'R') { if (data.bname !== '') { extraAddress += data.bname; } if (data.buildingName !== '') { extraAddress += extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; } fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; } setInput({ ...input, address: fullAddress, zipcode: data.zonecode }); onToggleModal(); }; const [uploadFile] = useMutation(UPLOAD_FILE); const [imgUrl, setImgUrl] = useState("") const fileRef = useRef<HTMLInputElement>(null) const onClickFile = () => { fileRef.current?.click() } const onChangeFile = async (e: ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; const isValid = checkFileValidation(file); if (!isValid) { return } if (isValid) { try { const result = await uploadFile({ variables: { file } }) setImgUrl(result.data?.uploadFile.url) } catch (error) { if (error instanceof Error) { Modal.error({ content: error.message }) } } } } const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(''); const [previewTitle, setPreviewTitle] = useState(''); const [fileList, setFileList] = useState<UploadFile[]>([]); const handleCancel = () => { setPreviewOpen(false); }; const handlePreview = async (file: UploadFile) => { if (!file.url && !file.preview) { file.preview = await getBase64(file.originFileObj as RcFile); } setPreviewImage(file.url ?? (file.preview as string)); setPreviewOpen(true); setPreviewTitle(file.url ? (file.name || file.url.substring(file.url.lastIndexOf('/') + 1)) : ""); }; return ( <BoardWrite_presenter onChangeInput={onChangeInput} onChangeCoreInput={onChangeCoreInput} onChangeImages={onChangeImages} writerError={writerError} passwordError={passwordError} titleError={titleError} contentsError={contentsError} zipcode={input.zipcode} address={input.address} onSubmit={onSubmit} onUpdate={onUpdate} valid={valid} isEditing={props.isEditing} data={data} isOpen={isOpen} handleComplete={handleComplete} onToggleModal={onToggleModal} imgUrl={imgUrl} onClickFile={onClickFile} onChangeFile={onChangeFile} fileRef={fileRef} fileList={fileList} handlePreview={handlePreview} setFileList={setFileList} previewOpen={previewOpen} previewTitle={previewTitle} handleCancel={handleCancel} previewImage={previewImage} /> ); } import { Button, Image, Modal, Upload } from 'antd'; import DaumPostcodeEmbed from 'react-daum-postcode'; import { Main, Title } from '../../../commons/styles/emotion'; import * as S from './BoardWrite_styles'; import { type IBoardWrite_presenter_Props } from './BoardWrite_types'; import type { UploadProps } from 'antd/es/upload'; import { PlusOutlined } from '@ant-design/icons'; export default function BoardWrite_presenter( props: IBoardWrite_presenter_Props ) { const valid = props.valid; const isEditing = props.isEditing; const data = props.data?.fetchBoard; const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { props.setFileList(newFileList); }; const uploadButton = ( <div> <PlusOutlined /> <div style={{ marginTop: 8 }}>Upload</div> </div> ); return ( <Main> <S.Form> <Title>게시물 {isEditing ? '수정' : '등록'}</Title> <div className="writer"> <S.InputWrapper> <label>작성자</label> <input id="writer" onChange={props.onChangeCoreInput} type="text" placeholder="이름을 입력해주세요." defaultValue={props.data?.fetchBoard.writer ?? ""} readOnly={!!props.data?.fetchBoard.writer} /> {props.writerError && ( <p className="alert">이름을 입력해주세요.</p> )} </S.InputWrapper> <S.InputWrapper> <label>비밀번호</label> <input id="password" onChange={props.onChangeCoreInput} autoComplete="off" type="password" placeholder="비밀번호를 입력해주세요." defaultValue={``} /> {props.passwordError && ( <p className="alert">비밀번호를 입력해주세요.</p> )} </S.InputWrapper> </div> <S.InputWrapper> <label>제목</label> <input id='title' onChange={props.onChangeCoreInput} type="text" placeholder="제목을 작성해주세요." defaultValue={data?.title} /> {props.titleError && ( <p className="alert">제목을 작성해주세요.</p> )} </S.InputWrapper> <S.InputWrapper> <label>내용</label> <textarea id='contents' onChange={props.onChangeCoreInput} placeholder="내용을 작성해주세요." defaultValue={props.data?.fetchBoard.contents} /> {props.contentsError && ( <p className="alert">내용을 작성해주세요.</p> )} </S.InputWrapper> <S.InputWrapper> <label>주소</label> <div className="zipcode"> <input id="zipcode" onChange={props.onChangeInput} type="text" // placeholder="00000" readOnly value={ props.address || (props.data?.fetchBoard.boardAddress?.address ?? "") } /> <button onClick={(e) => { e.preventDefault(); props.onToggleModal(); }} > 우편번호 검색 </button> {props.isOpen && ( <Modal title={'우편번호 검색'} open={props.isOpen} onOk={props.onToggleModal} onCancel={props.onToggleModal} > <DaumPostcodeEmbed onComplete={props.handleComplete} ></DaumPostcodeEmbed> </Modal> )} </div> <input id='address' onChange={props.onChangeInput} className="address" type="text" readOnly value={props.address !== "undefined" ? props.address : ""} /> <input id='addressDetail' onChange={props.onChangeInput} type="text" defaultValue={ data?.boardAddress?.addressDetail ? data?.boardAddress?.addressDetail : '' } /> </S.InputWrapper> <S.InputWrapper> <label>유튜브</label> <input id='youtubeUrl' onChange={props.onChangeInput} type="text" placeholder="링크를 복사해주세요." defaultValue={ data?.youtubeUrl ? data?.youtubeUrl : '' } /> </S.InputWrapper> <S.InputWrapper> <label>사진업로드</label> <Button onClick={props.onClickFile}>사진 업로드</Button> <input style={{ display: "none" }} ref={props.fileRef} onChange={props.onChangeFile} type="file" /> </S.InputWrapper> {props.imgUrl && <Image src={`https://storage.googleapis.com/${props.imgUrl}`}></Image>} <S.InputWrapper> <label>사진 첨부</label> <Upload // action="https://www.mocky.io/v2/5cc8019d300000980a055e76" listType="picture-card" fileList={props.fileList} onPreview={props.handlePreview} onChange={handleChange} > {props.fileList.length >= 8 ? null : uploadButton} </Upload> <Modal open={props.previewOpen} title={props.previewTitle} footer={null} onCancel={props.handleCancel}> <img alt={props.previewTitle} style={{ width: '100%' }} src={props.previewImage} /> </Modal> </S.InputWrapper> <S.InputWrapper> <div className="radios"> <span> <input type="radio" id="youtube" name="radios" value="youtube" defaultChecked onChange={props.onChangeImages} /> <label htmlFor="youtube">유튜브</label> </span> <span> <input type="radio" id="image" name="radios" value="image" onChange={props.onChangeImages} /> <label htmlFor="image">사진</label> </span> </div> </S.InputWrapper> <S.SubmitButton onClick={isEditing ? props.onUpdate : props.onSubmit} valid={valid} disabled={!valid} > {isEditing ? '수정하기' : '등록하기'} </S.SubmitButton> </S.Form> </Main> ); }