axios 통신 도중 delete에서 계속 400 에러 발생

요청 바디에 { clubName: '동아리명' } 형태로 보내야하는데 에러가 찍히지만 내가 원하는대로 잘 보내지길래 왜이러지 ? 싶었다

보내는 방식은 post에 요청 바디 보낼 때와 동일한 형태로 보냈음.

근데 저 에러에서 clubName 밑에 data: undefined가 조금 꺼림직했다.

지피티 최고 ㅠ

혹시나 하는 마음에 지피티에게 물어보니

DELETE 요청을 보낼 때도 요청 바디를 포함시킬 수 있다
>> 요청 바디를 포함 시키려면 data 옵션을 사용하라. 고 했다 !!!

 

그래서 아래의 형태로 보냈었던 요청을

const result = await API().delete('/admin/club', { clubName: name })

 

data 옵션을 사용해서 한번 더 감싸서 보내주니 !!!!

const result = await API().delete('/admin/club', {data: { clubName: name }})

 

반갑다 200아 ㅠ 해결완료 !

 

이전 포스팅에서 로그인 성공 후 응답으로 돌아온 토큰을 로컬스토리지에 저장하는 것까지 성공했다.

이후 로그인이 된 상태를 유지해야 하고 로그인 여부에 따라 다른 UI를 보여줘야 한다.

많은 페이지의 UI를 다르게 띄워줘야하는데 페이지마다 로컬 스토리지의 토큰을 get 해와서 처리하는 것은 번거로울 것 같았다.

 

그래서 생각해낸 방법은 useContext를 사용해서 전역으로 로그인 상태를 관리하는 것이었다.

 

 

로그인 된 상태 true / 로그인 하지 않은 상태 false 로 처리해서 전역변수 처리를 한 다음

다른 UI를 띄워줘야할 때 true/false 값을 활용해 간단하게 처리할 수 있는 것이다.

 

해야할 것은 
1. LoginContext.jsx 생성하여 전역 변수로 선언해주기
2. 로그인하면 isLoggedIn 값을 true로 변경
3. 로그아웃하면 isLoggedIn 값을 false로 변경
4. 다른 컴포넌트에서 isLoggedIn 값 활용하여 다른 UI 띄우기 !

1. LoginContext.jsx 생성하여 전역 변수로 선언해주기

import React, { createContext, useState, useContext, useEffect } from 'react';

const LoginContext = createContext();

export const LoginProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    // localStorage에서 토큰을 가져와서 유무를 판별하여 isLoggedIn 상태 설정
    const token = localStorage.getItem('Token');
    setIsLoggedIn(!!token); // 토큰이 있으면 true, 없으면 false
  }, []);

  return (
    <LoginContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
      {children}
    </LoginContext.Provider>
  );
};

export const useLogin = () => useContext(LoginContext);

전역으로 사용할 isLoggedIn 변수를 useState로 관리한다.

그리고 처음 렌더링 될 때, 로컬스토리지의 토큰을 가져와서 상태를 true/false로 저장한다.


이후 전역적으로 로그인 상태를 관리하기 위해 app.js의 컴포넌트들을 <LoginProvider>로 감싼다.

<LoginProvider>을 사용하면 하위의 여러 컴포넌트에서 동일한 로그인 상태를 사용하고 업데이트 할 수 있게 된다.

function App() {
  return (
    <LoginProvider>
      <div className="App textFont">
        <Routes>
          <Route path="/user/*" element={<User/>} />
            <Route path="/admin/*" element={<Admin/>} />
        </Routes>
      </div>
    </LoginProvider>
  );
}
export default App;

2. 로그인하면 isLoggedIn 값을 true로 변경

import { useLogin } from '../../context/LoginContext';

const { isLoggedIn, setIsLoggedIn } = useLogin();

// 로그인
const handleLogin = async () => {
  try {
    console.log(studentInfo)
    const result = await API().post('/login', studentInfo); // 로그인 성공
    navigate('/user'); // 사용자 메인으로 이동
    localStorage.clear()
    localStorage.setItem('Token', result.data.accessToken)

    setIsLoggedIn(true);
  } catch (error) {
    // 에러처리
  }
};

로그인 컴포넌트에 와서 사용하겠다. 선언해준뒤 로그인이 성공했을 때 값을 true 로 변경한다.


3. 로그아웃하면 isLoggedIn 값을 false로 변경

로그인과 거의 동일하다. 값만 false로 변경하면 된다.

const handleLogout = () => {
    localStorage.clear()
    setIsLoggedIn(false);
    console.log(isLoggedIn)
}

로그아웃하는 함수


4. 다른 컴포넌트에서 isLoggedIn 값 활용하여 다른 UI 띄우기 !

메뉴바의 마이페이지 아이콘을 클릭했을 때

로그인 상태면 > 마이페이지

로그인 상태가 아니라면 > 로그인 페이지로 이동하게끔 했다.

const { isLoggedIn, setIsLoggedIn } = useLogin();
const navLink = isLoggedIn ? 'myPage' : 'login';

상태에 따라 navLink의 값이 다르게 들어가고

<Link to={navLink}><AiOutlineUser size={30} className='mr-2'/></Link>

요렇게 링크를 해주면 끝!

로그인 했을 때와 안 했을 때

로그인 로직

동아리연합회와 협업 프로젝트를 진행하고 있고 그 중 로그인 부분을 진행하고 있다.

 

이번 로그인은 몇가지 조건이 있다.

- 동아리 사람들만 로그인할 수 있다. (동아리 소속 아닌 사람들은 로그인 자체가 안됨)

- 처음 로그인 하는 동아리원들은 동의 수집 페이지를 거쳐야 한다.

 

위의 항목들을 만족하지 않으면 에러로 돌아오고 에러 처리를 해줘야 한다.

 

정리해보면,

로그인 시도(/login)를 했을 때
- 동아리원O, 동의O -> 로그인 성공(200) -> 메인으로 이동
- 동아리원O, 동의X -> 401 에러 -> 동의 항목 페이지로 이동 ->  동의하기(/agree) -> 로그인 재시도(/login) -> 로그인 성공(200)
- 동아리원X -> 400 에러 -> 로그인 불가능

// 로그인
const handleLogin = async () => {
  try {
    console.log(studentInfo)
    const result = await API().post('/login', studentInfo); // 로그인 성공
    console.log(result);
    navigate('/user'); // 사용자 메인으로 이동
  } catch (error) {
    if (error.response) {
      const statusCode = error.response.status;
      if (statusCode === 400) { // 400 : 학번 or 이름 틀렸을 경우
        setFailModalOpen(!failModalOpen);
        setStudentInfo({
          studentId: '',
          name: ''
        });
      } else { // 401 : 개인정보 동의하지 않았을 경우
        setModalOpen(!modalOpen);
      }
    }
  }
};

 

로그인을 성공했을 때는 navigate로 페이지를 이동 시켜주고

실패했을 때는 에러 코드에 따라 다른 처리를 해주었다.

에러코드 === 400 > 동아리 소속이 아닌사람 (동아리원X)

해당 창이 뜨면서 로그인 실패

에러코드 === 401 > 동아리 소속이지만 동의항목을 거치지 않은 사람(동아리원O, 동의X)

개인정보 수집이용 동의 모달창을 띄워준다.

이후 모달창에서 동의를 누르면 서버(/agree)로 동아리원 정보가 전달되고 서버에서 전달받은 동아리원의 isAgree 항목을 true로 변경함.

// 동의했을 때
const handleAgree = async () => {
    try {
        await API().post('/agree', studentInfo); // 성공적으로 처리되면
        handleLogin(studentInfo); // 다시 로그인 함수 호출
    } catch (error) {
        console.log(error)
    }
}

이후 다시 로그인 재시도(/login)를 하면 동아리원O, 동의O 이므로 로그인 성공(200) !


로그인 성공시 돌아온 토큰 저장

const result = await API().post('/login', studentInfo); // 로그인 성공
    navigate('/user'); // 사용자 메인으로 이동
    localStorage.clear()
    localStorage.setItem('Token', result.data.accessToken)

로그인을 성공하게 되면 서버에서 토큰을 발행해주는데 이 토큰을 어딘가에 저장해두어야 한다.

일반적으로 발행된 토큰은 로컬 스토리지에 저장된다.

 

localStorage.clear()로 한번 비워주고,

localStorage.setItem(키, 값)의 형태로 돌아온 토큰을 저장하면

잘 저장되는 것을 볼 수 있다 ! 야호

전 포스팅인 카카오 로그인 iOS 편에서 기본 설정은 마쳤으므로 프로젝트 생성 및 라이브러리 설치는 생략되었습니다.

1. 카카오 플랫폼 등록

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

1-1. 패키지명 등록

프로젝트/android/app/src/main/java/com/프로젝트명/MainActivity.kt 파일 안의 1번째줄 > package com.프로젝트명

패키지명 : com~ 등록


프로젝트/android/app/src/main/AndroidManifest.xml 파일의 use-permission 태그의 패키지 속성 추가

<uses-permission android:name="android.permission.INTERNET" package="com.native01" />

 

1-2. 키 해시 등록

프로젝트/android/app/ 폴더로 이동한 뒤 터미널에 해당 코드 입력

keytool -exportcert -alias androiddebugkey -keystore debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64

코드 입력시 키 해시 값 나옴

 

패키지명, 키 해시 등록으로 안드로이드 플랫폼 등록 완료

2. Android 설정

2-1. KAKAO SDK 적용

android/build.gradle 파일 안에 코드 추가

maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' }

repositories 안에 추가~

2-2. Redirect URL 설정

android:allowBackup의 값을 true로 변경

android:allowBackup="true"

AndroidManifest.xml 파일에 코드 추가

- application 태그 안에 삽입

- 실제 자신의 카카오 네이티브 앱 키 삽입

<activity
   android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
   android:exported="true">
  <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />

      <data android:host="oauth"
          android:scheme="kakao{카카오 네이티브 앱 key를 입력해주세요}" />
  </intent-filter>
</activity>

3. Strings.xml

/android/app/src/main/res/values/strings.xml 파일에도 네이티브 앱 키 등록

4. android 실행

react-native run-android

흑 ... 성공 .... 감격스럽다

참고한 라이브러리 : https://github.com/crossplatformkorea/react-native-kakao-login

 

GitHub - crossplatformkorea/react-native-kakao-login: react-native native module for Kakao sign in.

react-native native module for Kakao sign in. Contribute to crossplatformkorea/react-native-kakao-login development by creating an account on GitHub.

github.com

1. 리액트 네이티브 프로젝트 생성

npx react-native init 프로젝트명

2. 앱 실행

npx react-native run-ios

3. 카카오 라이브러리 설치

npm add @react-native-seoul/kakao-login
npx pod install

 pod 라이브러리 설치

4. pod에서 iOS deployment target 11.0 이상으로 설정

> platform : ios, '11.0.0' 변경

5. 카카오 개발자 설정

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com


5-1. 플랫폼 설정

내 애플리케이션 > 애플리케이션 추가하기 > 앱이름, 사업자명 적고 저장하기

앱설정 > 플랫폼 > iOS 플랫폼 등록 > 번들 ID 작성

XCODE > Signing & Capabilities > Bundle Identifier 값 등록


5-2. 동의 설정

내 애플리케이션 > 제품 설정 > 카카오 로그인 > 활성화 설정 > 상태 ON

6. 프로젝트 설정

https://developers.kakao.com/docs/latest/ko/ios/getting-started

6-1. 앱 실행 허용 목록 설정

Info.plist 파일에 앱 실행 허용 목록(Allowlist)을 설정

 <key>LSApplicationQueriesSchemes</key>
  <array>
      <!-- 카카오톡으로 로그인 -->
      <string>kakaokompassauth</string>
      <!-- 카카오톡 공유 -->
      <string>kakaolink</string>
      <!-- 카카오톡 채널 -->
      <string>kakaoplus</string>
  </array>

6-2. 커스텀 URL 스킴 설정

[Info] > [URL Types] > [URL Schemes] 항목에 네이티브 앱 키(Native App Key)를 kakao${NATIVE_APP_KEY} 형식으로 등록

xcode에서 url 스킴 등록하면 자동으로 생성 됨


위의 두가지 설정 후 최종 info.plist 추가되는 코드

7. AppDelegate.m 파일 설정

#import <RNKakaoLogins.h>

- (BOOL)application:(UIApplication *)app
     openURL:(NSURL *)url
     options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
 if([RNKakaoLogins isKakaoTalkLoginUrl:url]) {
    return [RNKakaoLogins handleOpenUrl: url];
 }

 return NO;
}

import~ 코드는 위쪽에 그리고 밑 bool~ 코드는

@implementation AppDelegate 아래에 적어줘야한다.

8. Podfile

Podfile 파일 안의 target 프로젝트명 do ~ end 사이에 코드 추가

pod 'KakaoSDK'

9. 예제 코드

App.tsx

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * Generated with the TypeScript template
 * https://github.com/react-native-community/react-native-template-typescript
 *
 * @format
 */

import Intro from './pages/Intro';
import React from 'react';
import {
  SafeAreaView,
} from 'react-native';

const App = () => {

  return (
    <SafeAreaView >
      <Intro />
    </SafeAreaView>
  );
};

export default App;

Pages/Intro.tsx

import { Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useState } from 'react';
import { login, logout, getProfile as getKakaoProfile, shippingAddresses as getKakaoShippingAddresses, unlink } from '@react-native-seoul/kakao-login';
import ResultView from './IntroView';

const Intro = () => {
  const [result, setResult] = useState<string>('');

  const signInWithKakao = async (): Promise<void> => {
    try {
      const token = await login();
      setResult(JSON.stringify(token));
    } catch (err) {
      console.error('login err', err);
    }
  };

  const signOutWithKakao = async (): Promise<void> => {
    try {
      const message = await logout();

      setResult(message);
    } catch (err) {
      console.error('signOut error', err);
    }
  };

  const getProfile = async (): Promise<void> => {
    try {
      const profile = await getKakaoProfile();

      setResult(JSON.stringify(profile));
    } catch (err) {
      console.error('signOut error', err);
    }
  };

  const getShippingAddresses = async (): Promise<void> => {
    try {
      const shippingAddresses = await getKakaoShippingAddresses();

      setResult(JSON.stringify(shippingAddresses));
    } catch (err) {
      console.error('signOut error', err);
    }
  };

  const unlinkKakao = async (): Promise<void> => {
    try {
      const message = await unlink();

      setResult(message);
    } catch (err) {
      console.error('signOut error', err);
    }
  };

  return (
    <View style={styles.container}>
      <ResultView result={result} />
      <Pressable
        style={styles.button}
        onPress={() => {
          signInWithKakao();
        }}
      >
        <Text style={styles.text}>
          카카오 로그인
        </Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => getProfile()}
      >
        <Text style={styles.text}>
          프로필 조회
        </Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => getShippingAddresses()}
      >
        <Text style={styles.text}>
          배송주소록 조회
        </Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => unlinkKakao()}
      >
        <Text style={styles.text}>
          링크 해제
        </Text>
      </Pressable>
      <Pressable
        style={styles.button}
        onPress={() => signOutWithKakao()}
      >
        <Text style={styles.text}>
          카카오 로그아웃
        </Text>
      </Pressable>
    </View>
  );
};

export default Intro;

const styles = StyleSheet.create({
  container: {
    height: "100%",
    justifyContent: "flex-end",
    alignItems: 'center',
    paddingBottom: 100
  },
  button: {
    backgroundColor: '#FEE500',
    borderRadius: 40,
    borderWidth: 1,
    width: 250,
    height: 40,
    paddingHorizontal: 20,
    paddingVertical: 10,
    marginTop: 10
  },
  text: {
    textAlign: "center"
  }
});

IntroView.tsx

import { ScrollView, StyleSheet, Text, View } from 'react-native';

import React from 'react';

type Props = {
  result: string;
};

function IntroView({ result }: Props): React.ReactElement {
  return (
    <View style={styles.container}>
      <ScrollView>
        <Text>{result}</Text>
        <View style={{ height: 100 }} />
      </ScrollView>
    </View>
  );
}

export default IntroView;

const styles = StyleSheet.create({
  container: {
    flexDirection: "column",
    width: "100%",
    padding: 24,
  }
});

여기까지 다 하면 ~~~

iOS 카카오 로그인 성공 ㅠㅠㅠ

이거 때문에 프로젝트 몇 개를 생성했는지 모르겠다 .... 

천천히 하나하나 해보니까 됨 ..... 예 !!!

시도하는 모든 사람들 성공하시길 바랍니당

React

리액트 프로젝트 생성 : npx create-react-app 프로젝트명

리액트 실행 : npm start


React Native

ReactNative cli

리액트네이티브 프로젝트 생성 : npx react-native init 프로젝트명

ios 실행 : npx react-native run-ios

android 실행 : npx react-native run-android


expo cli

expo cli 설치 : npm install -g expo-cli

프로젝트 생성 : npx create-expo-app my-app

2024.03.23~03.24 이틀 간 무박2일 해커톤인 벚꽃톤을 진행했다

지금까지 해커톤을 몇 번 한 적이 있는데 이렇게 쌩으로 밤새면서 개발에 몰두해본 적은 처음인 것 같다 

23일 오후 2시에 시작해서 24일 오전 7시에 개발이 마감되었고,

이후 바로 데모부스, 아침식사, 각 팀 발표, 시상식, 럭키드로우를 거쳐 오후 5시 반쯤,, 끝이 났다

 

오후 2시부터 다음날 오전 7시까지 1,2,3차 스프린트 시간에는 해결하지 못한 이슈들이 많았기에

놀랍게도 시간이 정말 순삭되었다는 거.. 야식먹고 새벽 2시부터는 코드만 보다가 5시간이 훌쩍 .. 

주변에도 자는 사람이 거의 없었고 모두가 개발에 집중하던 시간이었다.


 

나는 21팀 카카오 ESG 경영 [TEAM KREWS] 의 프론트엔드 파트를 맡았고,

주제를 한 줄로 소개하자면 "🙌‘함께 성장’ - 크루의 가치를 증진하는 팀 빌딩 서비스"이다.

 

팀 프로젝트 중 생겼던 어려움에는 

1. 팀 이름을 결정하는데 많은 시간을 소모하는 것

2. 의견을 내지 못해 적극적인 팀플레이 감소

3. 업무 분배 실패로 팀 협동심 동기부여 하락

등이 있고, 

 

이러한 어려움을 해소하고자 [팀크루즈] 서비스에서는

1. 팀/프로젝트 이름 생성기 > 키워드 입력시 OpenAI API를 활용하여 이름 추천

2. 1:1 시크릿 피드백 > 익명으로 요구사항, 피드백 요청

3. 투두리스트  > 팀원이 함께 업무 투두리스트를 작성

등의 기능을 도입하고자 하였다.

 

여기서 나는 1:1 시크릿 피드백 기능을 맡게 되었다 !


AI 캠퍼스 도착 & 1차 스프린트

이때까지만 해도 아주 생기발랄하고 설렘반 기대반으로 스프린트를 시작했던 

사진도 찍고 캠퍼스 구경도 하면서 기분좋게 개발 시작 ㅎㅎㅎ

0123

2차 스프린트

생각보다 풀리지 않는 오류에 시간이 많이 잡아먹히고 야식 시간 전까지도 에러와의 싸움에 조금씩 지쳐간 ..

그래도 바로 옆에 소통할 수 있는 팀원들이 있어서 혼자 개발할 때보다 삽질의 시간이 줄었던 것 같다.

웹소켓 구현이 되다가 안되다가 .. 어디서 잘못됐는지 정확히 파악하지 못해서 더 시간을 많이 잡아먹었다.

 

야식 & 3차 스프린트

야식먹고 다들 조급해져서 최소한의 기능이라도 다 끝내보자는 생각으로 몰두했던 . 

다행히 주요 기능인 1:1 시크릿 피드백 채팅, openAI API 팀명 생성은 모두 구현이 되었고,

투두리스트는 개인 투두작성만 구현하고 끝이 났다 

완벽하게 마치지는 못했지만 그래도 마감시간까지 모두 최선을 다해서 개발했기에 후회는 없었다 !

다들 너무너무 수고했고,, 눈떠보니 아침이어서 놀랐지만 쉴 새도 없이 바로 데모부스 준비 ㅋㅋㅋ ㅋㅋ ㅠㅠ

01

데모부스 & 팀 별 발표

우리 서비스를 소개시켜주고, 다른 팀들의 부스들도 돌아다니며 구경했다

나름 스티커도 제작하고 디자인도 너무 예쁘게 뽑혀서 만족스러웠던 .. 

근데 옷 맞추거나 x배너 한 팀들 스케일 보고 놀랐.. ㅎㅎ

비몽사몽한 채로 부스에 오는 사람들한테 열심히 설명한다고 하긴 했는데 잘 전달되었을지 모르겠다

01

어찌저찌 팀 별 발표까지 다 마치고 대망의 심사위원 피드백 시간

이미 현업에서 많이 쓰고 있는 협업 툴들이 있는데 그것과의 차별성이 딱히 없다는 것

차이점이 있다면 팀명을 생성해주는 것 정도인 것 같다고 하셨다.워크스페이스를 생성하는 것도 1명이 하는 것은 팀 프로젝트의 의미가 퇴색된다는 점이 있었고.등등 피드백을 받으면서 처음 기획을 시작할 때 미처 생각하지 못했던 부분들을 짚어주셨다.

01

피드백을 받으면서 느낀점은 먼저 기획과 아이디어가 탄탄해야한다는 것이었다.

어느 방면으로도 들어올 수 있는 질문들에 대처할 수 있는 우리만의 기능이나 특이성이 있어야하는 것

이미 이 서비스의 페인포인트를 해결해줄 수 있는 서비스들이 있다면, 이것들과 차별화된 것들이 필요한데

눈에 띄는 차별성이 없었다는 것 .. 기능 개발에만 집중하면서 이것들을 많이 놓쳤던 것 같다. 

 

뭔가 아쉬움이 남는 프로젝트인 것 같지만, 이것도 하나의 경험이라고 생각한다

팀원들이랑 너무 좋은 팀워크를 이루었고, 최선을 다해서 완성한 것만으로도 충분히 만족스러운 !

 

디자인 너무너무 예쁘게 잘 뽑아준 주희랑 발표 대본 준비하고 앞에서 잘 설명해준 하영이, 같이 프론트작업하면서 역할 구분없이 부족한 부분 채워준 아진이랑, 옆에서 같이 에러 잡아준 백엔드 팀원 유재오빠랑 승언이까지 ~ 너무 다들 고생많았다 !! 

01

 

짧은 기간동안 얻어간 것이 너무 많았던 벚꽃톤,

다른 팀들의 서비스를 보고 많은 사람들을 만나면서 개발 뿐만아니라 다른 많은 것들을 느낄 수 있는 시간이었다. 

아직 부족하다는 것을 많이 느끼고 앞으로 더더 열심히 해야겠다는 원동력이 된 것 같은 . !

아무튼 무박 2일 벚꽃톤 끗 !

저기 어딘가에 내가 있음 .. 

'플젝 회고' 카테고리의 다른 글

2023 멋쟁이사자처럼 CSS 강의 회고  (0) 2024.01.12

크롤링한 데이터(정책) > 엑셀 > 데이터베이스 > 스프링 > 프론트에 띄우기 ==> 일단 이렇게 생각하고 작업 중이다.

데이터 양이 많아서 스프링 애플리케이션 내에서 메모리에 모두 유지하는 것보다는

데이터베이스에 저장하여 관리하는 것이 좋을 것 같아서 저러한 과정을 거치기로 한 것.

 

크롤링한 데이터를 엑셀 파일로 저장하는 과정은 생각보다 어렵지 않았다

1. 먼저 필요한 라이브러리 설치

  • pip install openpyxl 

2. 엑셀파일 만들고 저장

# 엑셀 만들기
import openpyxl

wb = openpyxl.Workbook()

ws = wb.create_sheet('주거정책')

ws['A1'] = 'number'
ws['B1'] = 'name'

ws['A2'] = 1
ws['B2'] = '홍길동'

wb.save(r'C:\Users\tmdgm\Desktop\pyex\주거정책_data.xlsx')

엑셀파일 만들기 > openpyxl.Workbook()

엑셀 워크시트 만들기 > wb.create_sheet('엑셀시트이름')

행, 열에 들어갈 데이터 추가한 후 wb.save('저장할 경로') 로 저장하면 해당 폴더에 엑셀 시트가 만들어지는 것을 볼 수 있다.

 

이 코드를 데이터 크롤링하는 코드와 합쳐보면

# 가져온 데이터 엑셀파일로 저장
import requests
from bs4 import BeautifulSoup
import openpyxl

fpath = r'C:\Users\tmdgm\Desktop\pyex\주거정책_data.xlsx'

wb = openpyxl.load_workbook(fpath)
ws = wb.active # 현재 활성화된 시트 선택 - 기본시트 선택

row = 2
for i in range(1, 5):
  response = requests.get(f'https://youth.incheon.go.kr/youthpolicy/youthPolicyInfoList.do?menudiv=dwelling&pgno={i}')
  html = response.text
  soup = BeautifulSoup(html, 'html.parser')
  titles = soup.select(".boardList .con-box .tit") # 정책 title
  links = soup.select(".boardList .btn-box .btn:first-child") # 정책 url
  
  # for link in links:
  #   url = link.attrs['href']
  #   print(f'https://youth.incheon.go.kr{url}')
    
  for title in titles:
    print(title.text.strip())
    ws[f'B{row}'] = title.text
    row += 1
    
wb.save(fpath)

 

1. 엑셀 파일을 불러와서

openpyxl.load_workbook(엑셀파일저장되어있는경로) 

2. 현재 활성화된 시트를 선택하고

wb.active

3. 크롤링한 데이터들을 행에 알맞게 저장한다.

ws.[f'B{rows}'] = title.text

B행에 쭉 저장되겠죠 ?

4. 저장하면 끝.

wb.save(fpath)

 

전 포스팅에서 공공데이터 API를 활용하여 정책들을 받아왔었다.

그거 하는 것도 꽤나 애먹었는데 내가 필요한 정보들이 아니었다.

그래서 여러 사이트를 검색해보던 중 청년 정책들을 잘 소개해주는 사이트를 발견했다.

카테고리 주거분야로 들어가면 주거 관련 정책들만 쫙 모아주니 여기서 데이터를 받아오면 좋겠다 생각했다.

그래서 이번에는 웹 크롤링을 시도해봤다. 이것저것 정말 많이 해보는 ...  (vsCode 사용했습니다.)

 

내가 할 것은
1. 위의 사이트에서 정책 제목들을 가져오는 것
2. 해당 정책의 상세보기에 접근할 수 있는 '사업 안내' 버튼 URL을 가져오는 것

1. 필요한 라이브러리 설치

크롤링에 기본이 되는 라이브러리들이다.

  • pip install requests
  • pip install beautifulsoup

파이썬 버전 문제때문에 시간을 또 잡아먹었는데

https://youtu.be/eJ7kqK18afY?si=cD5xM1nliGlPtFhW

이거보고 해결함 ㅠㅠ 

2. 크롤링하고 싶은 URL 가져오기

# 크롤링 기본
import requests
from bs4 import BeautifulSoup

# 크롤링하고 싶은 url get해오기
response = requests.get("https://youth.incheon.go.kr/youthpolicy/youthPolicyInfoList.do?")
html = response.text # html 전체 코드 들어있음.
soup = BeautifulSoup(html, 'html.parser') # html 번역기
# soup.select > 여러개 / soup.select_one > 한 개 선택
titles = soup.select(".tit")

for title in titles:
  print(title.text.strip())

 

설치한 라이브러리들을 import 해준다.

requests.get("URL")로 내가 크롤링하고 싶은 주소를 넣는다.

성공적으로 가져와졌다면 response 출력시 <Response [200]> 이 뜰 것이다.

이후 html번역기인 html.parser를 활용

soup.select() < css 선택자를 활용하여 가져오고 싶은 태그 선택

  • select > 한 개 선택
  • select_one > 여러 개 선택

.tit가 여러 개라면 리스트 형태로 titles에 들어온다.

반복문으로 title를 출력

성공 나이쓰 !

3. 링크 가져오기 

import requests
from bs4 import BeautifulSoup

response = requests.get("https://youth.incheon.go.kr/youthpolicy/youthPolicyInfoList.do?menudiv=dwelling")
html = response.text
soup = BeautifulSoup(html, 'html.parser')
titles = soup.select(".boardList .con-box .tit") # 정책 title
links = soup.select(".boardList .btn-box .btn:first-child") # 해당 정책 URL 접근

for link in links:
  url = link.attrs['href']
  print(f'https://youth.incheon.go.kr{url}')

위와 비슷한 코드이다.

다른 은 a태그 href 속성을 활용하여 url을 가져온 것

link에는 a 태그가 들어가 있다. 

link.attrs['href'] : 해당 링크의 url을 가져올 수 있다.

위의 '~ 인천시 청년월세 지원사업' 들의 해당 정책 상세보기 링크에 접근할 수 있는 것!

4. 여러 페이지 가져오기

정책들이 여러 페이지로 이루어져 있을 것이다.

페이지를 이동할 때마다 변경되는 url을 보고 활용하면 된다.

1페이지
2페이지

페이지가 이동될 때 pgno= 1, 2로 변경되는 것을 볼 수 있다.

# 여러 페이지 가져오기
import requests
from bs4 import BeautifulSoup

pageNum = 1 
for i in range(1, 10):
  print(f'{pageNum}페이지입니다.')
  response = requests.get(f'https://youth.incheon.go.kr/youthpolicy/youthPolicyInfoList.do?menudiv=dwelling&pgno={i}')
  html = response.text
  soup = BeautifulSoup(html, 'html.parser')
  titles = soup.select(".boardList .con-box .tit") # 정책 title
    
  for title in titles:
    print(title.text.strip())
    
  pageNum += 1

 

 

 

반복문으로 pgno={i} < 1~10페이지까지의 모든 정책 제목을 가져올 수 있었다.

간단하게 사이트 크롤링해보기 성공

앞으로의 여정은 멀고도 험하지만요 시작이 반이라고~

 

 

방학에 열심히 했어야했는데 발등에 불떨어진 ... 이제 졸업작품 열심히 준비해야겠다.

일단 내가 해야되는 부분은 공공데이터포털에서 청년 주거 정책을 가져오는 것이다.

 

https://www.data.go.kr/

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

여기 들어가서 끌어오고 싶은 공공데이터 신청하기

활용신청에 웹서비스개발 , 활용목적 간단하게 쓰면 바로 승인해준다.

새벽에 신청했는데 바로 승인됨.


승인되면 개발계정 상세보기에 있는 값들 활용하면 된다.

여기서 겁나 헤맸는데 ...  생각보다 간단하게 해결 ㅠ

1. 일반 인증키(디코딩) 복사
2. 인증키 설정 누르고 복사한 인증키를 붙여넣는다.


그리고 아래 제공해주는 스웨거에서

API 실행준비 > OpenAPI 호출하면 Response가 돌아온다.
저기서 Request URL 활용하면 된다.

위의 Request URL 활용해서 아래 코드 apiUrl 작성

apiUrl에 요청 변수를 분할해서 엔드포인트와 인증키, 반환 형식 등을 지정해주면 끝 !


package com.example.CheerupYouth_Back.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

@RestController
@Slf4j
public class ApiController {
    @GetMapping("/api")
    public String callApi() throws IOException {
        StringBuilder result = new StringBuilder();

        String apiUrl = "https://api.odcloud.kr/api/15038446/v1/uddi:4502e4a9-4db1-4169-bc21-feb0abb017a6?" +
                "pageNo=1" +
                "&serviceKey=인증키" + 
                "&perPage=100" +
                "&returnType=json"; // 반환 형식 json

        URL url = new URL(apiUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");

        int responseCode = connection.getResponseCode();

        if (responseCode == HttpURLConnection.HTTP_OK) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
            String line;
            while ((line = reader.readLine()) != null) {
                result.append(line);
            }
            reader.close();
        } else {
            result.append("API 호출 실패, 응답코드: ").append(responseCode);
        }

        log.info(result.toString());
        connection.disconnect();

        return result.toString();
    }
}

간단하게 API 받아오는 것까지는 성공.. .!


localhost:8080/api 로 접근하면 응답 데이터 들어와있는 것 확인!

 

 

성공은 했는데 .. 내가 필요한 정보들은 이게 아니라서 조금 당황했다. 

API가 아니가 주거정책 사이트를 크롤링 해와야하나 ....

+ Recent posts