728x90

브라우저마다 다른 css가 적용되어 있는 경우도 있고, 혹시 모를 경우를 방지하기 위해서 기본 브라우저의 css를 없애줄 필요가 있다. 이 작업을 실행하지 않는다면, 각 브라우저마다 내가 작업한 웹 페이지의 css가 조금씩 차이가 날 수 있다!

https://meyerweb.com/eric/tools/css/reset/
방법은  결국  모든 태그들에 대해서 기본css를 지정하는 것 이다. 해당 코드들은 위의 링크를 통해서 확인할 수 있다.

사이트마다 초기세팅이 어떻게 되었는지 정확히 알 수 없기 떄문에, 모든 태그에 대해서 등록하는것!

 

이를 쉽게 하기 위해서  styled-reset 라이브러리를 사용하는 방법도 있지만, 저는 제 프로젝트에서 styled-components 라이브러리를 사용하기 때문에 해당 라이브러리의 createGloablStyle을 활용하였다.

 

위의 링내용을 GLobalStyle에 등록한다

import { createGlobalStyle } from 'styled-components';

/// rest css 용
const GlobalStyle = createGlobalStyle`
html, body, div, span, applet, object, iframe,
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  a, abbr, acronym, address, big, cite, code,
  del, dfn, em, img, ins, kbd, q, s, samp,
  small, strike, strong, sub, sup, tt, var,
  b, u, i, center,
  dl, dt, dd, menu, ol, ul, li,
  fieldset, form, label, legend,
  table, caption, tbody, tfoot, thead, tr, th, td,
  article, aside, canvas, details, embed,
  figure, figcaption, footer, header, hgroup,
  main, menu, nav, output, ruby, section, summary,
  time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
  }

  /* HTML5 display-role reset for older browsers */
  article, aside, details, figcaption, figure,
  footer, header, hgroup, main, menu, nav, section {
    display: block;
  }

  /* HTML5 hidden-attribute fix for newer browsers */
  *[hidden] {
    display: none;
  }

  body {
    line-height: 1;
  }

  menu, ol, ul {
    list-style: none;
  }

  blockquote, q {
    quotes: none;
  }

  blockquote:before, blockquote:after,
  q:before, q:after {
    content: '';
    content: none;
  }

  table {
    border-collapse: collapse;
    border-spacing: 0;
  }

  //style custom
  * {
    box-sizing: border-box;
  }

  body {
    font-family: 'Source Sans Pro', sans-serif;
    background-color: ${(props) => props.theme.bgColor};
    color: ${(props) => props.theme.textColor}
  }

  a {
    text-decoration: none;
  }
	`;

export default GlobalStyle;

이때 저는 추후  사이트의 테마 색 까지 고려하여서 themeprovider도 같이 적용하였습니다.

 

import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';

import { basicTheme } from './assets/styles/theme';
import App from './App';
import GlobalStyle from './assets/styles/GlobalStyle';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
	<ThemeProvider theme={basicTheme}>
		{/* <QueryClientProvider client={client}> */}
		<BrowserRouter>
			<GlobalStyle />
			<App />
		</BrowserRouter>
		{/* </QueryClientProvider> */}
	</ThemeProvider>,
);

index.tsx 폴더에 ThemProvide를 등록하고 <GlobalStyle/>을  호출하면 적용할 수 있습니다.

    background-color: ${(props) => props.theme.bgColor};
    color: ${(props) => props.theme.textColor}

GlobalStyle의 위 코드는 테마 적용시 기본색을 바꾸기 위해서 props 형식으로 내려받도록 하였습니다.

우선 저희 프로젝트에서는 색을 아래와 같이 지정하였습니다.

import { DefaultTheme } from 'styled-components';

// colors  TODO: 변수명 수정, 추후 테마 변경 적용하려면 코드 추가
export const color = {
	color1: '#845EC2',
	color2: '#A178DF',
	color3: '#BE93FD',
	color4: '#DCB0FF',
	color5: '#FACCFF',
};

// 추후 테마색 결정
export const basicTheme: DefaultTheme = {
	bgColor: 'white',
	textColor: 'black',
};

추후 테마색을 적용하기 위해선 color를 수정해야겠습니다.


728x90

axios에서는 api 호출시에 중복되는 코드나 기능을 생략할 수 있는 axiosinstance라는 기능이 있습니다.
이를 활용하면 코드의 유지보수성과 확장성을 얻을 수 있습니다.

axios 공식 홈페이지가 설명이 굉장히 친절하게 되어 있어서 직접 확인 하는 것도 좋습니다.
https://axios-http.com/kr/docs/intro

 

import axios, { InternalAxiosRequestConfig } from 'axios';

const axiosInstance = axios.create({
  baseURL: 'http://localhost:3000/',
  timeout: 100000,
  headers: {
    'content-type': 'application/json;charset=UTF-8',
    accept: 'application/json,',
  },
});

axiosInstance.interceptors.request.use(
  (config: InternalAxiosRequestConfig<{ headers: string }>) => {
    const access_token = sessionStorage.getItem('authorization');

    // config.headers 초기화
    config.headers = config.headers || {};
    if (access_token !== null) {
      config.headers['Authorization'] = `${access_token}`;
    }
    return config;
  }
);

export default axiosInstance;

위 코드는  서버에 api호출시에 로그인을 했다면 저장된 토큰을 자동으로 전송하기 위해서 작성한 코드입니다.
이는 api호출시 request단계에서 매번 토큰을 불러오는 과정을 직접 입력할 필요가 없어지죠.

사실 axios 자체가 타입 추론이 잘되어서 별다른 코드를 작성하지 않고 타입가드 정도만 해도 정상적으로 작동합니다. 
하지만 이럴 경우 any 타입으로 선언이 되기 때문에 타입스크립트의 장점이 드러나지 않습니다.

따라서 저는 제가 사용하는 토큰 전송시에만 간단하게 타입 처리를 하였습니다.

초기 config는 InternalAxiosRequestConfig 타입에 any값이 제네릭에 들어오는 것을 알 수 있습니다.

저는 이때 config 안에 있는 header를 사용하기 때문에 

axiosInstance.interceptors.request.use(
  (config: InternalAxiosRequestConfig<{ headers: string }>) => {
    const access_token = sessionStorage.getItem('authorization');

    // config.headers 초기화
    config.headers = config.headers || {};
    if (access_token !== null) {
      config.headers['Authorization'] = `${access_token}`;
    }
    return config;
  }
);

로 수정했습니다.

이처럼 타입 추론이 굉장히 잘 되기 때문에, 원하시는 혹은 추가하고 싶은 타입이 있다면  해당 부분에 마우스를 올려서 어떤 값이 any로 들어오고 있는지 확인하고 이를 수정하시면 됩니다.

 


https://velog.io/@bang9dev/axios-interceptor-with-ts 에서 타입을 굉장히 꼼꼼히 쓰신 예시를 볼 수 있는데, 참고하시는 것도 좋을 것 같습니다!

 

Axios 인터셉터 타입스크립트에서 제대로 쓰기

Axios 에는 interceptors 라는 기능이 있다. 이를 통해서 request/response 에 선행, 후행 처리를 커스텀하게 할 수 있다. 하지만 interceptor 를 통해서 변경된 값이 타입에 적용되지는 않는데, 이를 멋지게

velog.io

 

728x90

 

아래는 바이트 공식 홈페이지에 가면 볼 수 있는 vite의 장점들이다. 바이트란 무엇일까?

https://vitejs.dev/

 

이전까지 npx create-react-app을  통해서 리액트 앱을 만들었는데 ,  vite는 이를 대신해서 사용할 수 있는 것이다.
물론 리액트 뿐만 아니라 뷰 같은 다른 프로젝트도 생성이 가능하다.


그렇다면 왜 바이트를 사용할까?

공식 문서내용은 아래와 같고 요약하자면
기존에 ES모듈(import)이 사용하술 있기 전에는 번들링을 통해서 , 소스 모듈을 브라우저에서 실행할 수 있는 파일로 크롤링, 처리 및 연결했다. 그런데 이러한 방식이  점점 자바스크립트 코드가 많아지면서 병목현상이 생겼다. 그 당시엔 좋았떤 것이 점차 속더 및 성능 저하가 생긴 것이다.

 

이를 ES모듈을 적용하여서 속도 및 성능을 개선한 게  바이트다.

 

바이트는 느린 서버시작과 업데이트를 개선함

 

서버시작 개선방법 

Depencies 부분 

자바스크립트는 ES번들을 사용하여 종속성을 사전 번들로 제공하고, ES번들GO로 작성되었는데 이는 자바스크립트 기반 번들보다 10~100이상 속도가 빠르다.

 

Sourcecode 부분

Native ESM을 통해서 소스코드 제공 + 브라우저가 일부 번들링 작업을 인계받고, 브라우저가 요청시에만 제공

 

서버업데이트 개선방법 

기존 번들러 기반 빌드 설정에서는 파일 편집 시 전체 번들을 다시 빌드하는 경우가 있는데 , vite가 사용하는
HMR방식은 페이지의 나머지 부분에 영향을 주지않고 변경된 모듈 자체를 교체해서 빠르게 화면에 반영되게 함

또한 ESM을 통해서 실행하여서 기존 HMR방식과는 다르게 앱의 크기에도 덜 영향을 받는다.

 

소스코드를 작성하여 변경하더라도 state가 초기화 되지않음.

설치방법

npm init vite
(처음 사용하는 경우 y를 누르면  create vite가 설치됨)

이후 원하는 프로젝트 (react ) -> typescript를 선택한 후
npm install로 모듈 설치
이후 npm run dev로 실행 (npm start 대신 사용)

패캠 강사님이 비교한 것에 의하면
                          바이트      vs        CRA              의 차이는 
설치 시간               30초                  1분40초
FIle Size                45MB                  221MB
Build File Size       150KB                553KB

728x90

시작하기

네이버 클라우드 회원가입이후에 콘솔 버튼을 통해서 네이버 클라이드가 제공중인 AI API를 확인할 수 있다.

  

이후 콘솔창에서 service -> chatbot을 클릭하면 

도메인 생성창으로 이동하는데, 도메인 생성을 할 경우 아래와 같이 뜬다. (저는 11469 도메인을 만들었습니다)

 

이후 자기 도메인의 우측의 빌더 실행하기를 통해서 ChatBot을 시작할 수 있다.

 

 

응답 만들기

위에 노란색으로 색칠된 버튼을 통해서 대화를 만들고 확인할 수 있다.

우산 대화생성을 통해서 특정 입력값이 들어왔을 때, 어떻게 답변할 지 설정할 수 있다.

 

대화이름을 설정하고, 어떤 질문을 할지 결정한 다음에, 관련 답변을 등록하는 식으로 챗봇이 대답하게 할 수 있다.

이때 일반적인 대화로 할 경우에는 등록해야할 값이 너무 많아진다.

따라서 정규식을 이용하면 편하다.

정규식의 상세한 내용은 정규식 가이드를 보면서 만들면 된다.

이후 답변을 등록할 수 있는데, 
이때 사용할 수 있는 답변으로는

이 있다.

 

이 외에도 폼을 통해서 추가적인 답변을 사용할 수 있다.

폼을 이용하면 , 답변식에 주고 받는 형태의 대화를 미리 등록할 수 있다.

나는 기분이라는 폼을 만들었는데 이후에

이를 답변 등록하면

특정 질문에 대한 답변으로 사용가능하다.


테스트하기

테스트는 우측하단 테스트를 통해서 할 수 있다.

하지만 태스트 전에 우선 상단의 대화모델 빌드를 해야한다.

 

테스트 결과가 제대로 나온다면 서비스 배포를 해야한다.

 

배포하기

자바에서 해당 챗봇을 사용하기 위해선 배포를 해야한다 

그러기 위해선 아래 사진의 1번 챗봇설정, 이후 메신저 연동을 누르고  2번을 클릭하면 된다. ( Line이나 특정 앱에서 사용하려면 다른거 클릭)

이후 시크릿키 생성이후 해당 모달에서 나온 시크릿키와 주소를 나중에 있을 자바 코드에 사용하면 된다.

그리고 상단의 서비스 배포를 누르고 배포를 시작한다.

 

자바코드

 

채널 연동 가이드를 통해서 자세한 사용법을 확인할 수 있다.

예제 코드 링크 

https://api.ncloud-docs.com/docs/ai-application-service-chatbot-chatbot#api-%EC%98%88%EC%A0%9C

 

CLOVA Chatbot Custom API

 

api.ncloud-docs.com

 

여기서 하단의 java 예시를 응용하여 코드를 만들었다.

 

package main;

import android.util.Base64;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.Timestamp;
import java.util.Date;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import android.util.Base64;

import org.json.JSONArray;
import org.json.JSONObject;


public class MainClass {

	public static void main(String[] args) {
		 String chatbotMessage = "";
		    String voiceMessage = "ㅎㅇ";

	        try {
	            String apiURL = "내uri복붙";
	            String secretKey = "시크릿키복붙";
	            URL url = new URL(apiURL);
	            
	            String message = getReqMessage(voiceMessage);
//	            String message = voiceMessage;
	            System.out.println("##" + message);

	            String encodeBase64String = makeSignature(message, secretKey);

	            HttpURLConnection con = (HttpURLConnection)url.openConnection();
	            con.setRequestMethod("POST");
	            con.setRequestProperty("Content-Type", "application/json;UTF-8");
	            con.setRequestProperty("X-NCP-CHATBOT_SIGNATURE", encodeBase64String);

	            // post request
	            con.setDoOutput(true);
	            DataOutputStream wr = new DataOutputStream(con.getOutputStream());
	            wr.write(message.getBytes("UTF-8"));
	            wr.flush();
	            wr.close();
	            int responseCode = con.getResponseCode();

	            BufferedReader br;

	            if(responseCode==200) { // Normal call
	                System.out.println(con.getResponseMessage());

	                BufferedReader in = new BufferedReader(
	                        new InputStreamReader(
	                                con.getInputStream()));
	                String decodedString;
	                while ((decodedString = in.readLine()) != null) {
	                    chatbotMessage = decodedString;
	                }
	                //chatbotMessage = decodedString;
	                in.close();

	            } else {  // Error occurred
	                chatbotMessage = con.getResponseMessage();
	            }
	        } catch (Exception e) {
	            System.out.println(e);
	        }
	        
	        //choatbot 응답
	        System.out.println(chatbotMessage);
	    }

	 public static String makeSignature(String message, String secretKey) {

	        String encodeBase64String = "";

	        try {
	            byte[] secrete_key_bytes = secretKey.getBytes("UTF-8");

	            SecretKeySpec signingKey = new SecretKeySpec(secrete_key_bytes, "HmacSHA256");
	            Mac mac = Mac.getInstance("HmacSHA256");
	            mac.init(signingKey);

	            byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));
	            encodeBase64String = Base64.encodeToString(rawHmac, Base64.NO_WRAP);

	            return encodeBase64String;

	        } catch (Exception e){
	            System.out.println(e);
	        }

	        return encodeBase64String;

	    }

	    public static String getReqMessage(String voiceMessage) {

	        String requestBody = "";

	        try {

	            JSONObject obj = new JSONObject();

	            long timestamp = new Date().getTime();

	            System.out.println("##"+timestamp);

	            obj.put("version", "v2");
	            obj.put("userId", "U47b00b58c90f8e47428af8b7bddc1231heo2");
	//=> userId is a unique code for each chat user, not a fixed value, recommend use UUID. use different id for each user could help you to split chat history for users.

	            obj.put("timestamp", timestamp);

	            JSONObject bubbles_obj = new JSONObject();

	            bubbles_obj.put("type", "text");

	            JSONObject data_obj = new JSONObject();
	            data_obj.put("description", voiceMessage);

	            bubbles_obj.put("type", "text");
	            bubbles_obj.put("data", data_obj);

	            JSONArray bubbles_array = new JSONArray();
	            bubbles_array.put(bubbles_obj);

	            obj.put("bubbles", bubbles_array);
	            obj.put("event", "send");

	            requestBody = obj.toString();

	        } catch (Exception e){
	            System.out.println("## Exception : " + e);
	        }

	        return requestBody;

	    }

}

해당 예제는 안드로이드 음성까지 참조할 수 있도록 작성되어서 변수들을 그냥 일반 텍스트로 사용할 수 있도록 변경하였다.

이때 maven repository

https://mvnrepository.com/artifact/com.googlecode.json-simple/json-simple

https://mvnrepository.com/artifact/org.json/json

https://mvnrepository.com/artifact/net.morimekta.utils/android-util

 

세가지를 다운받고

build path를 통해서 external library를 classpath에 등록하면 정상 작동한다.

 

결과물

##1692582719863

##{"bubbles":[{"data":{"description":"ㅎㅇ"},"type":"text"}],"event":"send","version":"v2","userId":"U47b00b58c90f8e47428af8b7bddc1231heo2","timestamp":1692582719863}

OK

{"version":"v2","userId":"U47b00b58c90f8e47428af8b7bddc1231heo2","timestamp":1692582720267,"bubbles":[{"type":"template","data":{"cover":{"type":"text","data":{"description":"오늘 기분이 어떠세요"}},"contentTable":[[{"rowSpan":1,"colSpan":1,"data":{"type":"button","title":"좋아요","data":{"type":"basic","action":{"type":"postback","data":{"postback":"UnexpiredForm␞77228␞0","displayText":"좋아요","postbackFull":"_T_UnexpiredForm␞77228␞0"}}}}}],[{"rowSpan":1,"colSpan":1,"data":{"type":"button","title":"별로에요","data":{"type":"basic","action":{"type":"postback","data":{"postback":"UnexpiredForm␞77228␞1","displayText":"별로에요","postbackFull":"_T_UnexpiredForm␞77228␞1"}}}}}]]},"information":[{"key":"imageOnly","value":"false"},{"key":"chatType","value":"SINGLEFORM"},{"key":"chatType","value":"SINGLEFORM"},{"key":"score","value":"1.0"},{"key":"scenarioName","value":"챗봇테스트"},{"key":"endOfBubble","value":"endOfBubble"},{"key":"matchingType","value":"exactMatch"},{"key":"domainCode","value":"ChatBotTestKSH"},{"key":"formStart","value":"true"}],"context":[]}],"scenario":{"name":"챗봇테스트","chatUtteranceSetId":5434254,"intent":[]},"entities":[],"keywords":[],"conversation":{"scenarioName":"챗봇테스트","chatUtteranceSetId":5434254,"types":[]},"normalizer":"null","event":"send"}

'벡엔드' 카테고리의 다른 글

spring으로 프로젝트 준비하기  (0) 2023.07.13
728x90

패스트 캠퍼스에서   swiper 라이브러리 사용에 대해서 배우게 되었다. (패캠 프론트엔드 초격차 강의)

공식 문서는 

https://swiperjs.com/react

 

Swiper - The Most Modern Mobile Touch Slider

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

사용한 예시 코드는 다음과 같다.

 <Container>
      <h2>{title}</h2>
      <Swiper
        // install Swiper modules
        modules={[Navigation, Pagination, Scrollbar, A11y]}
        loop={true} //loop 기능을 사용할 것인지
        navigation // arrow 버튼 사용 유무
        pagination={{ clickable: true }} //페이지 버튼 보이게 할지
        breakpoints={{
          1378: {
            slidesPerView: 6, //한번에 보이는 슬라이드 개수
            slidesPerGroup: 6,
          },
          998: {
            slidesPerView: 5, //한번에 보이는 슬라이드 개수
            slidesPerGroup: 5,
          },
          625: {
            slidesPerView: 4, //한번에 보이는 슬라이드 개수
            slidesPerGroup: 4,
          },
          0: {
            slidesPerView: 3, //한번에 보이는 슬라이드 개수
            slidesPerGroup: 3,
          },
        }}
      >
        <Content id={id}>
          {movies?.map((movie) => (
            <SwiperSlide key={movie.id}>
              <Wrap>
                <img
                  key={movie.id}
                  src={`https://image.tmdb.org/t/p/original${movie.backdrop_path}`}
                  alt={movie.name}
                  onClick={() => handleClick(movie)}
                />
              </Wrap>
            </SwiperSlide>
          ))}
        </Content>
      </Swiper>

      {modalOpen && (
        <MovieModal {...movieSelected} setModalOpen={setModalOpen} />
      )}
    </Container>

이중에서 Container와 Content Wrap태그는 styled-components 라이브러리를 사용해서 만든 스타일드 컴퍼넌트입니다.

 

 

 

좌측 Usage 탭에서 모듈들을 확인할 수 있다. (강의랑 import 주소가 다릅니다 'swiper/modules'로 수정) --> 매번 달라질 수 있어서 공식문서 확인하는게 좋아요

 

내가 적용한 모듈은 

 modules={[Navigation, Pagination, Scrollbar, A11y]}

각각의 모듈은

  • Navigation - Navigation module
  • Pagination - Pagination module
  • Scrollbar - Scrollbar module
  • A11y - Accessibility module

공식 사이트 예시 코드에서도 그렇고 기분적으로 사용하는 친구들입니다.

이후 style 탭을 통해서 확인하고 자기가 사용하고 싶은 css를 임포트 하면 됩니다

// import swiper style
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/scrollbar';
import 'swiper/css/pagination';
import styled from 'styled-components';

이후
Swiper 안에서 map 메서드를 통해서 각각의 SwiperSlide를 생성하면 된다.

 

좌우의 흰 버튼이 Swiper의 navigation 이고 , 우측 하단의 점들이 pagination이다.

 

Swiper 에 들어가는 어트리뷰트는 아래 링크에서 확인할 수 있다.

https://swiperjs.com/swiper-api#parameters

 

Swiper - The Most Modern Mobile Touch Slider

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

 

breakpoints 는 화면 크기에 따라 보여주는 것을 어떻게 다르게 할 지 설정하는 것이다.

loop는 슬라이더가 끝에 갔을 떄 루프를  통해 다시 처음으로 이동가능하게 하는 것이다.

 

728x90

알고리즘을 풀면서 문제를 초기에 구상을 했을 때는, 더 빠른 방법이지 않을까 생각했는데, 직접해보니 오히려 추가 작업만 늘어나게 되었던 경우를 포스팅하려고 합니다.
//  https://school.programmers.co.kr/learn/courses/30/lessons/87389

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

위 문제는 나머지가 1이 되는 수를 찾는 문제입니다.

초기 구상 단계

 

1. 나머지가 1이되는 최소의 수를 구하는 것이기에, 차라리 n-1이 소수인지만 확인 
2. 그렇지 않다면  해당 수를 반환 .. 해당 수의 약수가 존재한다면 최소공약수를 반환
3. 범위는 타겟의 제곱근까지만 확인( 그 이상은 확인할 필요가 없으므로)
이라고 생각했다. 그렇기에 소수를 구하는 에란토스체를 사용하면 런타임을 최소화 할 수 있지 않을까 생각했습니다.

 

우선 간단하게 타겟 수의 제곱근보다 작은수들로 n-1을 나누어 보았습니다.

function solution2(n) {
  const min = n - 1;
  for (let i = 2; i < Math.sqrt(n); i++) {
    if (min % i === 0) {
      return i;
    }
  }
  return min;
}

 

이후 이전에 학습했던 에란토스체를 사용해서 나누어 보았습니다.

 

function solution2(n) {
  const min = n - 1;
  const prime = new Array(n + 1).fill(1);

  for (let i = 2; i < Math.sqrt(n); i++) {
    if (prime[i]) {
      if (min % i === 0) {
        return i;
      }
      // 배수값들 거르는 작업
      for (let j = i ** 2; j <= n; j += i) {
        if (prime[j]) prime[j] = 0;
      }
    }
  }
  return min;
}

 

오히려 더 느려졌네요.. 왜 그럴까요? 에란토스체를 소수를 판별하는 가장 빠른 방법인데?

 

결론부터 말하면 부가적인 작업이 오히려 많이 생겼기 떄문입니다.

 

문제의 목적은 사실 n-1의 최소공약수를 구하는 문제입니다.  n의 제곱근보다 작은 모든 소수를 구한 다음에 나눌 필요가 없는거죠. 

 

소수를 구하는 단계에서는 각각의 수가 소수인지 일일이 확인하는 것보다, 미리 소수의 배수들을 제외시키면 소수인지 판별하기 훨신 쉽습니다.

하지만 이 경우 n-1이 소수인지만 판별하면 되는데, 부가적인 작업이 오히려 더 많이 생긴 것 입니다.
그렇기에 오히려 간단한 첫 번쨰 코드의 성능이 가장 좋았던 것이죠.



추가적으로  위 코드에서는 Math.sqrt의 계산을  반복문 내에서 수행할 필요 없이, 반복문 밖에서 한 번만 계산하도록 최적화 할 수 있습니다.


function solution3(n) {
  const min = n - 1;
  const prime = new Array(n + 1).fill(1);
  const limit = Math.sqrt(n);
  for (let i = 2; i < limit; i++) {
    if (prime[i]) {
      if (min % i === 0) {
        return i;
      }
      // 배수값들 거르는 작업
      for (let j = i ** 2; j <= n; j += i) {
        if (prime[j]) prime[j] = 0;
      }
    }
  }
  return min;
}

 

따라서 첫 번째 정답도 가장 최적화를 한다면 아래 코드가 됩니다.

function solution(n) {
  const min = n - 1;
const limit =  Math.sqrt(n);
  for (let i = 2; i <limit; i++) {
    if (min % i === 0) {
      return i;
    }
  }
  return min;
}

728x90

프로젝트를 하다보면 issue를 통해서 팀원과 소통하는 일이 생긴다.


우리 조는 GITHUB을 통하여서 issue 이름과 내용을 작성하고,  pull request 시에 해당 이슈를 태그하는 방식으로 이슈 추적을 하려고 했다. 하지만 이때 개개인이 다르게 issue를 작성한다면 이슈 내용 파악에 불편함을 느낄 수 있다.

따라서 통일된 Templates를 통해서 가이드라인을 제시한다면 팀원끼리 소통하는데 조금 더 수월할 수 있다.

Issue Templates 생성하기

 

깃헙에서 내 issue를 생성하고 싶은 레퍼지토리에 들어가서 setting을 누르고 스크롤 하면

Features에  Set up templates를 찾을 수 있다.

 

해당 버튼을 누르면 기초 템플릿을 설정할 수 있다.

 

결국 커스텀 가능하지만, 기본적인 모양이 갖춰져 있는 Feature request를 선택하고 수정하는 것을 추천합니다. (버그 수정용 템플릿이라면 위에껄 선택)

 

 

기본적인 양식은 위와 같은데 제목옆에 연필 버튼을 누르면 수정이 가능합니다.

위의 템플릿이 어떤 형식으로 되었는지 볼 수 있습니다. 위 코드와 비교해보면 **이 굵게 한다는것도 알 수 있네요

 

꾸미는 자세한 방법

https://docs.github.com/ko/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax 에서 확인할 수 있습니다.

 


작성을 완료하고 issue를 처음 발행하려고 하면 템플릿 설정창이 뜹니다.

해당 파일을 선택하면  내가 작성했던 템플릿이 issue 작성시마다 불러와 지는것을 확인할 수 있습니다.



 

하지만 이렇게 만들 경우에는
매번 getStarted를 통해서 작성하는 불편함이 생긴다. 레포지토리를 보면 아래와 같은 구조로 되어있다.
(해당 폴더안에 있는 파일들을 자동으로 제안함)

이를 default 값을 미리 설정하고 싶다면 작성했단 이슈 파일명을 issue_template.md 로 변경하고 , 이전에 생성했던 폴더를 제거하면 된다.


pull_request_template 또한 동일한 과정을 거쳐 작업하면 된다.

728x90

이런 모양의  달력을 라이브러리를 통해서 간단하게 만들 수 있다.

 

해당 라이브러리의 깃헙주소는 https://www.npmjs.com/package/fullcalendar 이다.

설치 

해당 라이브러리를 시작하기 위해서는 우선 세 가지를 인스톨 해야한다.

npm install --save  @fullcalendar/core @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/list

npm install --save @fullcalendar/interaction

npm install --save-dev css-loader

  

이후 프로젝트에서 사용하면 된다.

 

저는 프로젝트를 만든 이후 , component 폴더에 달력과 관련 css파일만 추가해서 실행하였습니다.

 

 

이제 코드를 보겠습니다. 

import React, { useRef } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import listMonth from "@fullcalendar/list";
import interactionPlugin from "@fullcalendar/interaction";

import "./Calendar.css";

const Calendar = () => {
  const calendarRef = useRef();
  // event 추가
  const onEventAdd = (e) => {
    const api = calendarRef.current.getApi();
    api.addEvent({
      title: "이사장님과의 약속",
      date: "2023-08-24 12:30",
      constraint: "약속",
    });
  };
  // 이벤트 클릭시
  const eventCLick = (info) => {
    // alert("eventClick");
    // alert(JSON.stringify(info.event.start));

    let date = new Date(info.event.start);
    let year = date.getFullYear();
    let month = date.getMonth();
    let day = date.getDate();
    let hour = date.getHours();
    let minute = date.getMinutes();
    let promise = `날짜 : ${year}년 ${month + 1}월 ${day}일 ${hour}시 ${minute}분
`;
    promise += "제목 : " + info.event.title + "\n";
    promise += "내용 : " + info.event.constraint;
    alert(promise);
  };

  // 날짜를 클릭시
  const handleDateClick = (args) => {
    let date = new Date(args.dateStr);
    let year = date.getFullYear();
    let month = date.getMonth() + 1 + "";
    if (month.length < 2) {
      month = "0" + month;
    }
    let day = date.getDate() + "";
    if (day.length < 2) {
      day = "0" + day;
    }
    let selectDate = year + "-" + month + "-" + day;
    alert(selectDate);
  };

  return (
    <div className="calendar">
      <FullCalendar
        headerToolbar={{
          left: "prev next today", // 좌측버튼
          center: "title",
          end: "dayGridMonth timeGridWeek timeGridDay listMonth",
        }}
        locale={"ko"} // 한국어
        navLinks={true} // 오른쪽 상단의 day 클릭 대신 날짜를 클릭
        businessHours={true} // 주말을 다른 색으로
        plugins={[dayGridPlugin, timeGridPlugin, listMonth, interactionPlugin]}
        initialView="dayGridMonth"
        eventClick={eventCLick}
        ref={calendarRef}
        dateClick={handleDateClick}
        events={[
          {
            title: "점심약속",
            date: new Date(), //'2023-08-16
          },
          {
            title: "미팅",
            start: "2023-08-14", //'2023-08-16 까지 적으면 줄이 김
          },
          {
            title: "미팅2",
            start: "2023-08-14 12:30", //'2023-08-16 까지 적으면 줄이 김
          },

          {
            title: "비지니스",
            start: "2023-08-18 12:30:00",
            constraint: "김사장과 복싱",
            end: "2023-08-20 12:30",
          },
          {
            title: "워크샵",
            start: "2023-08-17 12:30:00",
            constraint: "팔협지 또 일등이야",
            end: "2023-08-22 12:30",
          },
          {
            title: "데이트",
            start: "2023-08-27 12:30:00",
            constraint: "영화관람",
            backgroundColor: "#ff0000",
          },
        ]}
      />

      <br />
      <button onClick={onEventAdd} className="btn btn-primary">
        일정추가
      </button>
    </div>
  );
};

export default Calendar;

약식으로 만든 파일이기 때문에 위와 같이 작성을 하였습니다.
FullCalendar를 제외한 플러그인은 컴퍼넌트가 아니기에 소문자로 import 했고
FullCalendar는 대문자로 import해야합니다

 

그리고 현재는 서버와의 통신없이 코드를 작성하였기에 , 

 const onEventAdd = (e) => {
    const api = calendarRef.current.getApi();
    api.addEvent({
      title: "이사장님과의 약속",
      date: "2023-08-24 12:30",
      constraint: "약속",
    });
  };

에서 임의의 더미데이터를 넣어 작성하였지만, 서버와 연결을 할 거면 axios를 통해서 데이터를 전송하시면 됩니다.

 

이제 라이브러리에 대해 설명하겠습니다.

<FullCalendar
        headerToolbar={{
          left: "prev next today", // 좌측버튼
          center: "title",
          end: "dayGridMonth timeGridWeek timeGridDay listMonth",
        }}
        locale={"ko"} // 한국어
        navLinks={true} // 오른쪽 상단의 day 클릭 대신 날짜를 클릭
        businessHours={true} // 주말을 다른 색으로
        plugins={[dayGridPlugin, timeGridPlugin, listMonth, interactionPlugin]}
        initialView="dayGridMonth"
        eventClick={eventCLick}
        ref={calendarRef}
        dateClick={handleDateClick}
        events={[
          {
            title: "점심약속",
            date: new Date(), //'2023-08-16
          },
          {
            title: "미팅",
            start: "2023-08-14", //'2023-08-16 까지 적으면 줄이 김
          },
          {
            title: "미팅2",
            start: "2023-08-14 12:30", //'2023-08-16 까지 적으면 줄이 김
          },

          {
            title: "비지니스",
            start: "2023-08-18 12:30:00",
            constraint: "김사장과 복싱",
            end: "2023-08-20 12:30",
          },
          {
            title: "워크샵",
            start: "2023-08-17 12:30:00",
            constraint: "팔협지 또 일등이야",
            end: "2023-08-22 12:30",
          },
          {
            title: "데이트",
            start: "2023-08-27 12:30:00",
            constraint: "영화관람",
            backgroundColor: "#ff0000",
          },
        ]}
      />

headerToolbar 의 경우 헤더에 있는 버튼들을 정의해줍니다. 이때 prev 와 next 는 화살표 모양으로 달력이동, today는 현재 날짜로 돌아오게 합니다.  left title right는 해당 버튼들의 위치를 정의하고,  dayGridMonth timeGridWeek timeGridDay listMonth 는 각각 월 주 일 일정리스트별 정렬을 해줍니다.

week을 클릭할 경우

 

locale은 언어 설정,

navlinks는 day대신 날짜 클릭 (false하는게 좋다고 생각합니다), 

businessHours는 주말은 다른색,

plugins에 위에 import한 plugin들을 연결하고,

initial view는 시작시 보여줄 달력의 모양,
dateClick은 달력의 날짜 클릭시 효과,

eventClick은 일정 클릭시 효과입니다.

 

728x90

리엑트로 위와 같은 페이지네이션을 구현했습니다. 이때 사용한 라이브러리와 코드를 공유하려고 합니다.

우선 라이브러리이기에 프로젝트에 해당 라이브러리를 먼저 설치해야합니다.

npm i react-js-pagination

 

라이브리러의 자세한 정보는 https://mui.com/material-ui/react-pagination/ 에서 확인하면 됩니다!

 

이후 사용하고자 하는 페이지에서 import 합니다.

import Pagination from "react-js-pagination";

Pagination 이 컴퍼넌트 이기에 소문자로 import하면 제대로 작동하지 않습니다.

 

 <Pagination
		// 내가 선언한 상태 : page, totalCnt    함수 : handlePageChange
        activePage={page} 
        itemsCountPerPage={10}
        totalItemsCount={totalCnt}
        pageRangeDisplayed={5}
        prevPageText={"이전"}
        nextPageText={"다음"}
        onChange={handlePageChange}
      />

태그에 대한 자세한 설명은 https://mui.com/material-ui/api/pagination/ 에서 확인하시면 됩니다.

제가 사용한 것은 

 

activePage : 현재 페이지

itemsCountPerPage : 한 페이지에 보여줄 글 수

totleItemsCount = 총 게시글 수

pageRangeDisplayed = 한 번에 보여줄 page 버튼 수 (저는 게시글이 적어서 2개만 나왔습니다)

?? +PageText : 다음 혹인 이전 버튼에 들어갈 글자

onChange : page가 선택됐을 때 실행할 함수 

 

 위치는 본인 프로젝트에 맞게 넣으면 됩니다.  저는 글 쓰기 버튼 위 게시판 아래에 배치하였습니다.

 

// 페이징
  const [page, setPage] = useState(1);
  const [totalCnt, setTotalCnt] = useState(0);

useState를 통해서 현재 페이지와, 총 글 수를 변수 선언했고

const getBbslist = (c, s, pn) => {
    axios
      .get("http://localhost:3000/bbslist", { params: { choice: c, search: s, pageNumber: pn } })
      .then((resp) => {
        console.log(resp.data);
        setBbslist(resp.data.bbslist);
        setTotalCnt(resp.data.cnt); // 글의 총수
      })
      .catch((err) => {
        alert(err);
      });
  };

게시글을 불러오는 함수를 통해서 게시글 데이터를 get요청했습니다.

이때 resp로 bbslist(게시글 리스트) 와 cnt(총 게시글 수) 를 리턴 받습니다.
controller에서는 서비스에 있는 bbslist 함수를 통해서 게시글들을 반환하고, getallbbs 함수를 통해서 총 게시글 수를 반환합니다.

@GetMapping("bbslist")
	public Map<String,Object> bbslist(BbsParam param){
		System.out.println("BbsController bbslist " + new Date());
		//글목록
		List<BbsDto> list = service.bbslist(param);
		
		//글의 총수
		int count = service.getallbbs(param);

		// 현재 페이지
		Map<String,Object> map = new HashMap<String,Object>();
		map.put("bbslist", list);

		
		//react용
		map.put("cnt", count);
		return map;
	}

 

728x90

npx create-react-app  react-test 
                                   ( 프로젝트 명) 을 통해서 리액트 프로젝트를 생성하면 아래와 같은 구성이 생긴다.

이때 리엑트는  노란색 칠해진 3부분에 의해서 작동한다.

기본적으로 웹페이지는 index.html에서 시작을 하는데 리엑트에서도 마찬가지이다.

하지만 리엑트의 특징은 컴퍼넌트를 통해서 하나의 index.html 페이지에서 컴퍼넌트 교체를 통해서 다른 페이지를 보여주는 거 처럼 작동한다.

<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>

index.html에 보면 다음과 같이 적혀있다.

이때 npm start(혹은 yarn)대신 직접 열면, root 하위의 파일들이 불러와지지 않아서, 빈 페이지가 보인다.

 

index.html에 하위 컴퍼넌트를 연결하는 역할을 index.js가 한다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

이떄 StrictMode는 좀 더 엄격하게 프로젝트를 사용하기 위해 설정하는 것으로, 굳이 없어도 된다.

코드를 보면 html에 있던 id가 root인 곳(body)에 <App/> 을 랜더링한다고 적혀 있다.

그리고 App이 무엇인지는 App.js 파일을 보면 알 수 있다.

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

현재 우리가 봤던 화면은 위 코드를 통해 구성되어있다.

그리고 이를 App.css와 저장된 이미지에 애니메이션 효과를 주어서 우리가 봤던 아래 이미지를 구현한다.

컴퍼넌트는 이때 대문자로 이름이 시작해야하고, export default를 통해서 외부 파일에서 사용할 수 있게 한다.

 

// index.js의 임포트 부분

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

index.js 에서 ./App 을 통해서 export한 App을 사용하고 있다. 이때 뒤에 './App'; 은 경로인데, .js의 경우는 생략이 가능하다.

+ Recent posts