세모세 프로젝트_마무리

jkky98·2023년 12월 6일
0

Project

목록 보기
10/21
post-thumbnail

창조는 모방부터 시작이라고 색 조합을 오픈소스 템플릿 디자인에서 크롬 내장 플러그인 colorpicker로 가져와서 색을 다 바꿔주었다. 또한 기본 모드를 dark모드로 바꾸었다.
역시 흰 바탕에 검은 글씨보단 검은 바탕의 흰 글씨가 좋다.(극히 개인 취향) 오른쪽의 크롤링된 매물들은 양이 너무 많아 특정 기준으로 정렬을 한다해도 이용자가 이 전체 매물을 느끼기에는 부족하다 생각해서 아래에 지도를 하나 더 띄워서 지도에 크롤링한 매물들을 띄우고, 지도에 마커들을 띄워서 해당 마커를 클릭하면 주소가 입력되도록 기능을 추가했다.

클릭이벤트가 전달되었음을 알리기 위해 누르면 마커가 빨간점으로 바뀌게 했다. 이렇게 주소와 주변시설 설정을 완료하고 상세정보 탭으로 들어가면,

다음과 같은 분석 페이지가 등장한다. 맵에서 score와 각각의 거리를 나타내주긴 하지만, 지도로는 한계가 있다고 생각해서 상세정보 페이지를 만들었다. 현재 선택한 매물이 뜨고, 매물에 대한 정보와 계산된 점수가 백엔드로부터 얻어진다. 오른쪽의 차트의 경우 선택한 외부주변시설에 대해 가장 가까운 것들을 가져와서 거리를 계산해서 나타내준다. 차트로는 chart.js를 사용해서 따로 컴포넌트를 구성했다.

<template>
  <Bar
    :options="chartOptions"
    :data="chartData"
    :chart-id="chartId"
    :dataset-id-key="datasetIdKey"
    :plugins="plugins"
    :css-classes="cssClasses"
    :styles="styles"
    :width="width"
    :height="height"
  />
</template>

<script>
import { Bar } from "vue-chartjs";
import { mapState } from "vuex";
import {
  Chart as ChartJS,
  Title,
  Tooltip,
  Legend,
  BarElement,
  CategoryScale,
  LinearScale,
} from "chart.js";

ChartJS.register(
  Title,
  Tooltip,
  Legend,
  BarElement,
  CategoryScale,
  LinearScale
);

export default {
  name: "BarChart",
  components: { Bar },
  computed: {
    ...mapState({
      myAddress: "address",
      buttons: "buttons",
      zickbang_point: "zickbang_point",
      data: "receivedData",
      data_info: "receivedData_info",
    }),
  },
  props: {
    chartId: {
      type: String,
      default: "bar-chart",
    },
    datasetIdKey: {
      type: String,
      default: "label",
    },
    width: {
      type: Number,
      default: 1000,
    },
    height: {
      type: Number,
      default: 930,
    },
    cssClasses: {
      default: "",
      type: String,
    },
    styles: {
      type: Object,
      default: () => {},
    },
    plugins: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      chartOptions: {
        responsive: true,
        maintainAspectRatio: true,
        legend: {
          labels: {
            fontColor: "red",
            fontSize: 18,
          },
          color: "white",
        },
        indexAxis: "y",
        scales: {
          y: {
            ticks: {
              color: "white", // y 축의 텍스트 색상을 여기서 변경할 수 있습니다.
            },
            title: {
              display: true,
              text: "시설",
              color: "white",
              font: {
                size: 10,
              },
            },
          },
          x: {
            ticks: {
              color: "white", // x 축의 텍스트 색상을 여기서 변경할 수 있습니다.
            },
            title: {
              display: true,
              text: "거리(분)",
              color: "white",
              font: {
                size: 10,
              },
            },
          },
        },
      },
      chartData: {
        labels: ["스타벅스", "아주대학교", "이마트"],
        datasets: [
          {
            label: "Your Store List",
            backgroundColor: "#f87979",
            data: [14, 7, 12],
          },
        ],
      }, // chartData를 빈 객체로 초기화합니다.
    };
  },
  mounted() {
    // 차트가 처음 마운트될 때 차트 객체를 참조합니다.
    this.$nextTick(() => {
      this.chart = this.$refs.myChart;
    });
  },
  watch: {
    data_info: {
      deep: true,
      handler(newDataInfo) {
        if (newDataInfo && newDataInfo.info_store) {
          // info_store 데이터를 차트 데이터로 변환하여 할당합니다.
          this.chartData = this.convertInfoStoreToChartData(
            newDataInfo.info_store
          );
          this.$nextTick(() => {
            if (this.chart) {
              this.chart.update();
            }
          });
        }
      },
    },
  },
  methods: {
    convertInfoStoreToChartData(infoStore) {
      return {
        labels: infoStore.map((item) => item.name), // 이름
        datasets: [
          {
            label: "Your Store List",
            backgroundColor: "#f87979",
            data: infoStore.map((item) => item.distance), // 거리 데이터
          },
        ],
      };
    },
  },
  // 
};
</script>

다음 chart.js에서 주는 기본 코드를 활용해서 제작했다. 항상 뭘 추가할 때마다 라이프사이클 고려가 아직 어려운 것이 문제이다. 나름의 디버깅으로 문제 부분에 nextticks를 때려넣고 있는데 정확히 어떤 문제인지 나중에 파악해야할 것 같다.

아래에는 추천리스트를 주었는데, vuetify UI에서 제공하는 Data Table을 활용했다.
추천 기준은 같은 클러스터 기준이다. 클러스터의 경우 K-means를 사용했다. 계산비용이 적은 것으로 인해 얻는 속도가 우수하며, 비교해본 결과 다른 모델과 큰 성능차이가 나타나지 않는 것 같았다. 클러스터의 개수를 결정하기 위해서 elbow그래프와 실루엣 계수등을 분석했다.

cluster의 N은 4로 결정했으며, 단점인 random centroid문제를 개선해서 성능을 올려보고자 kmeans++를 적용했다. pkl모델파일로 저장해서 백엔드에서 요청시에 요청데이터를 모델에 넣고 결과 데이터를 주는데 단순하게 피처에 cluster피처를 추가해서 크롤링데이터에 대해 cluster피처를 반환하는 식이다. 이 cluster피처를 통해 선택한 외부시설에 해당하는 cluster와 같은 cluster를 추천해서 리스트업했다.

kmeans는 복잡한 차원에서 성능이 떨어지므로 가격데이터들을 하나의 피처로 합쳤다. 기존에는 보증금, 월세, 관리비, 평수등이 있었는데 이것을 국가에서 제공되는 부동산공식을 활용해서 월비용/평수 로 환산해서 사용했다. 그 결과 클러스터링에서 가격적 요소에 대한 의존이 떨어진 것을 확인할 수 있었다.

또한 펼치기 버튼을 누르면 아래에 직방에서 보기, 이 매물 주소에 적용 버튼이 주어진다. 각각은, 해당 매물id에 해당하는 직방으로의 이동, 이 매물 주소에 적용은 다시 조건설정 페이지에 갈 필요 없이, 추천된 매물을 바로 적용해서 주변 시설을 확인하고 점수를 확인할 수 있게 했다. 또한 각 칼럼마다 정렬기능이 존재해서 추천리스트들 중에 원하는 데이터들을 우선적으로 볼 수 있게 했다.

이용자는 클러스터의 구분기준이 유사도인지 모르기도하고 유사도라는 것이 어떤게 유사하다는건지 모를 수 있기에 따로 클러스터의 가격적 정보를 제시하고자 했다. chart.js의 버블차트를 이용해서 하위 컴포넌트를 구성했으며, 코드는 다음과 같다.

<template>
  <Bubble
    :options="chartOptions"
    :data="clusterInfo"
    :chart-id="chartId"
    :dataset-id-key="datasetIdKey"
    :plugins="plugins"
    :css-classes="cssClasses"
    :styles="styles"
    :width="width"
    :height="height"
    :key="chartReloadKey"
  />
</template>

<script>
import { Bubble } from "vue-chartjs";

import {
  Chart as ChartJS,
  Title,
  Tooltip,
  Legend,
  PointElement,
  LinearScale,
} from "chart.js";

ChartJS.register(Title, Tooltip, Legend, PointElement, LinearScale);

export default {
  name: "BubbleChart",
  components: {
    Bubble,
  },
  props: {
    chartId: {
      type: String,
      default: "bubble-chart",
    },
    datasetIdKey: {
      type: String,
      default: "label",
    },
    width: {
      type: Number,
      default: 400,
    },
    height: {
      type: Number,
      default: 400,
    },
    cssClasses: {
      default: "",
      type: String,
    },
    styles: {
      type: Object,
      default: () => {},
    },
    plugins: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      chartReloadKey: 0,
      chartData: {
        datasets: [
          {
            label: "Data One",
            backgroundColor: "#f87979",
            data: [
              {
                x: 20,
                y: 25,
                r: 5,
              },
              {
                x: 40,
                y: 10,
                r: 10,
              },
              {
                x: 30,
                y: 22,
                r: 30,
              },
            ],
          },
          {
            label: "Data Two",
            backgroundColor: "#7C8CF8",
            data: [
              {
                x: 10,
                y: 30,
                r: 15,
              },
              {
                x: 20,
                y: 20,
                r: 10,
              },
              {
                x: 15,
                y: 8,
                r: 30,
              },
            ],
          },
        ],
      },
      chartOptions: {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: {
            ticks: {
              color: "white", // x 축의 텍스트 색상을 여기서 변경할 수 있습니다.
              min: 0, // Set the minimum value for the x-axis
              max: 3, // Set the maximum value for the x-axis
              stepSize: 1, // Set the step size to 1 to display only integer values
            },
            title: {
              display: true,
              text: "Cluster", // X 축 레이블 텍스트 설정
              color: "white",
              size: 10,
            },
          },
          y: {
            ticks: {
              color: "white", // x 축의 텍스트 색상을 여기서 변경할 수 있습니다.
              suggestedMin: 0, // Set the minimum value for the y-axis
              max: 20, // Set the maximum value for the y-axis
              step: 4, // Set the step size to 4 to display only integer values
            },
            title: {
              display: true,
              text: "평당 월세(만원)", // Y 축 레이블 텍스트 설정
              color: "white",
              size: 10,
            },
          },
        },
      },
    };
  },
  watch: {
    clusterInfo(newValue) {
      this.clusterData = newValue; // 클러스터 데이터 업데이트
      this.chartReloadKey += 1; // 차트 리로딩을 위한 키 값 증가
    },
  },
  computed: {
    clusterInfo() {
      const others = this.$store.getters.getMyOnerooms(false);
      if (others.length === 0) {
        return []; // others 배열이 비어있으면 빈 배열 반환
      }

      const newArray = others.map((item) => {
        const deposit = item.deposit;
        const rent = item.rent;
        const manage_cost = item.manage_cost;
        const area_p = item.area_p;
        const cluster = item.cluster;

        const costperRaw =
          ((deposit * 0.07 * 1.05) / 12 + rent + manage_cost) / area_p;

        const costper = parseFloat(costperRaw.toFixed(2));

        // 기존 객체의 속성을 복사하여 새로운 객체를 만듭니다.
        const newItem = {
          cluster: cluster,
          costper: costper,
        };

        return newItem;
      });
      // cluster 별로 그룹화하여 평균과 개수 계산
      const resultArray = [];
      for (let i = 0; i < 4; i++) {
        // cluster가 0부터 9까지의 정수라고 가정
        const filteredItems = newArray.filter((item) => item.cluster === i);
        const totalCount = filteredItems.length;
        const totalCostper = filteredItems.reduce(
          (acc, val) => acc + val.costper,
          0
        );
        const averageCostper = totalCostper / totalCount || 0;

        resultArray.push({
          x: i,
          y: averageCostper,
          r: totalCount,
        });
      }

      // r 값을 로그 스케일로 변환
      const logScaledData = resultArray.map((item) => ({
        x: item.x,
        y: item.y,
        r: Math.sqrt(item.r) + 20, // 로그 스케일로 변환
      }));

      const result_final = {
        datasets: [
          {
            label: "Cluster INFO",
            backgroundColor: "#f87979",
            data: logScaledData,
            pointRadius: 5, // 포인트 반지름 설정
            pointHoverRadius: 7, // 호버시 포인트 반지름 설정
            pointBorderWidth: 2, // 포인트 테두리 두께 설정
            pointBorderColor: "#fff", // 포인트 테두리 색상 설정
            pointBackgroundColor: "rgba(248, 121, 121, 0.8)", // 포인트 배경 색상 설정
          },
        ],
      };

      return result_final;
    },
  },
};
</script>

세권정보 페이지의 코드는 다음과 같다.

<template>
  <div class="page-background">
    <v-container fluid>
      <v-card class="pa-3">
        <v-row>
          <v-col cols="6">
            <v-card
              class="pa-3"
              outlined
              elevation="10"
              width="100%"
              color="#26293C"
              dark
            >
              <v-card-title class="justify-center text-h5 font-weight-bold"
                >SCORE</v-card-title
              >
              <!-- 데이터가 있을 때 -->
              <v-card
                v-if="
                  data_info && data_info.scorebox && data_info.scorebox.score
                "
              >
                <v-card-text class="text-center text-h1 font-weight-bold">
                  {{ data_info.scorebox.score }}
                </v-card-text>
                <!-- 데이터가 있을 때 -->
                <v-card
                  v-if="
                    data_info && data_info.scorebox && data_info.scorebox.score
                  "
                >
                  <v-card
                    class="pa-3"
                    outlined
                    width="100%"
                    height="100%"
                    color="#26293C"
                    dark
                  >
                    <v-card-title
                      class="justify-center text-h5 font-weight-bold"
                      >Present Address</v-card-title
                    >
                    <v-card-title class="title">
                      {{ myOnerooms[0].title }}
                    </v-card-title>
                    <v-card-subtitle class="subtitle">
                      아주대로 부터 {{ myOnerooms[0].distance.toFixed(1) }} 분
                      거리에 있습니다.
                    </v-card-subtitle>
                    <v-card-text>
                      <div>면적(평): {{ myOnerooms[0].area_p }}평</div>
                      <div>층: {{ myOnerooms[0].floor }}층</div>
                      <div>
                        보증금(전,월세): {{ myOnerooms[0].deposit }}만원
                      </div>
                      <div>관리비 : {{ myOnerooms[0].manage_cost }}만원</div>
                      <div>월세: {{ myOnerooms[0].rent }}만원</div>
                      <div>전/월세: {{ myOnerooms[0].sales_type }}</div>
                      <div>
                        등록시간:
                        {{ formatToLocalTime(myOnerooms[0].reg_date) }}
                      </div>
                      <v-btn
                        block
                        :href="
                          'https://www.zigbang.com/home/oneroom/items/' +
                          myOnerooms[0].item_id
                        "
                        target="_blank"
                        color="#F87979"
                        >직방에서 보기</v-btn
                      >
                    </v-card-text>
                  </v-card>
                  <!-- 여기에 데이터 표시하는 다른 요소들 추가 가능 -->
                </v-card>

                <!-- 데이터가 없을 때 -->
                <v-card v-else>
                  <v-card-text class="text-center text-h5 font-weight-bold">
                    조건설정을 완료해주세요.
                  </v-card-text>
                  <!-- 맵 로딩을 위한 요소들 추가 가능 -->
                </v-card>
                <!-- 여기에 데이터 표시하는 다른 요소들 추가 가능 -->
              </v-card>

              <!-- 데이터가 없을 때 -->
              <v-card v-else>
                <v-card-text class="text-center text-h5 font-weight-bold">
                  조건설정을 완료해주세요.
                </v-card-text>
                <!-- 맵 로딩을 위한 요소들 추가 가능 -->
              </v-card>
            </v-card>
          </v-col>
          <v-col cols="6">
            <v-card class="pa-0" outlined>
              <!-- <v-img src="@/assets/cluster_img.jpg"></v-img> -->
              <v-card
                class="pa-3"
                outlined
                elevation="10"
                width="100%"
                color="#26293C"
                dark
              >
                <bar-chart />
              </v-card>
            </v-card>
          </v-col>
        </v-row>
        <v-divider></v-divider>
        <v-col col="6">
          <v-card class="pa-3" outlined width="100%" color="#26293C" dark>
            <v-card-title class="justify-center text-h5 font-weight-bold"
              >Recommend List</v-card-title
            >
            <v-data-table
              :headers="headers"
              :items="filteredData"
              :items-per-page="5"
              class="elevation-1"
              multi-sort
              :sort-desc="[false, true]"
              loading
              loading-text="Loading... Please wait"
              :single-expand="singleExpand"
              :expanded.sync="expanded"
              show-expand
              item-key="item_id"
            >
              <template v-slot:top>
                <v-toolbar flat>
                  <v-toolbar-title
                    >현재 클러스터 :
                    {{ myOnerooms[0].cluster }}</v-toolbar-title
                  >
                  <v-spacer></v-spacer>
                  <v-switch
                    v-model="singleExpand"
                    label="Single expand"
                    class="mt-2"
                  ></v-switch>
                </v-toolbar>
              </template>
              <template v-slot:expanded-item="{ headers, item }">
                <td :colspan="headers.length">
                  <v-btn
                    block
                    :href="
                      'https://www.zigbang.com/home/oneroom/items/' +
                      item.item_id
                    "
                    target="_blank"
                    >직방에서 보기</v-btn
                  >
                  <v-btn
                    block
                    @click="settingaddress(item.lat, item.lng, item.title)"
                    >이 매물 주소에 적용
                  </v-btn>
                </td>
              </template>
            </v-data-table>
          </v-card>
        </v-col>
        <v-col cols="12">
          <v-card
            class="pa-3"
            outlined
            elevation="10"
            width="100%"
            color="#26293C"
            dark
          >
            <bubble-chart />
          </v-card>
        </v-col>
        <v-col cols="12">
          <v-card
            class="pa-3"
            outlined
            elevation="10"
            width="100%"
            color="#26293C"
            dark
          >
            <v-img src="@/assets/cluster_img.jpg"></v-img>
          </v-card>
        </v-col>
      </v-card>
    </v-container>
    <v-snackbar v-model="snackbar" :multi-line="multiLine">
      조건설정이 필요합니다.

      <template v-slot:action="{ attrs }">
        <v-btn color="red" text v-bind="attrs" @click="snackbar = false">
          Close
        </v-btn>
      </template>
    </v-snackbar>
  </div>
</template>

<script>
import { mapState } from "vuex";
import BarChart from "@/components/BarChart.vue";
import BubbleChart from "@/components/BubbleChart.vue";
import axios from "axios";

export default {
  data: () => ({
    multiLine: true,
    snackbar: false,
    cluster_percost_list: [],
    expanded: [],
    singleExpand: false,
    headers: [
      {
        text: "직방 매물",
        align: "start",
        sortable: false,
        value: "title",
      },
      { text: "거리(분-보행)", value: "distance" },
      { text: "월세(만원)", value: "rent" },
      { text: "보증금(만원)", value: "deposit" },
      { text: "평수(평)", value: "area_p" },
      { text: "관리비(만원)", value: "manage_cost" },
      { text: "층수(층)", value: "floor" },
      { text: "", value: "data-table-expand" },
    ],
    choose_zickbang: [{ lat: null, lng: null }],
    thenData: null,
    myData: [
      {
        title: "아주대 인근 원룸",
        distance: 10,
        area_p: 10,
        floor: 3,
        deposit: 1000,
        rent: 100,
        manage_cost: 5,
        sales_type: "월세",
        reg_date: "2021-05-01T00:00:00.000Z",
        item_id: 1083533,
        my: true,
      },
    ],
  }),
  watch: {
    // data_info.scorebox.score의 변경을 감지
    "data_info.scorebox.score": {
      handler(newVal) {
        // 데이터가 없으면 snackbar를 보여줌
        this.snackbar = !newVal;
      },
      immediate: true, // 초기화 시 즉시 실행
    },
  },
  async mounted() {
    try {
      let dataToSend = null;
      console.log(this.myAddress);
      console.log(this.zickbang_point);

      if (
        !this.myAddress &&
        Array.isArray(this.zickbang_point) &&
        this.zickbang_point.length > 0
      ) {
        dataToSend = this.zickbang_point;
      } else if (
        this.myAddress &&
        (!this.zickbang_point ||
          (Array.isArray(this.zickbang_point) &&
            this.zickbang_point.length === 0))
      ) {
        dataToSend = this.myAddress;
      } else {
        throw new Error("주소 또는 zickbang_point가 필요합니다.");
      }

      const res = await axios.post(`http://localhost:8000/api/info`, {
        address: dataToSend,
        selected: JSON.stringify(this.buttons),
      });

      console.log("---axios Post 성공---- /info ");
      const receivedData = res.data;

      const processedData = {
        scorebox: receivedData.scorebox,
        info_store: JSON.parse(receivedData.info_store),
        address_latlng: receivedData.address_point,
        bus: JSON.parse(receivedData.bus_data),
        oneroom_all: JSON.parse(receivedData.oneroom_data_cluster),
        oneroom_data_cluster: receivedData.my_data,
      };

      // Vue 데이터에 할당
      this.thendata = processedData;

      // Vuex 스토어에 데이터 저장
      this.$store.commit("SET_RECEIVED_DATA_INFO", processedData);
      console.log("postthen vuex 저장 완료");
    } catch (error) {
      console.error(error.message);
      // 에러 메시지 처리 등
    }
  },
  components: {
    "bar-chart": BarChart,
    "bubble-chart": BubbleChart,
  },
  computed: {
    ...mapState({
      myAddress: "address",
      buttons: "buttons",
      zickbang_point: "zickbang_point",
      data: "receivedData",
      data_info: "receivedData_info",
    }),
    myOnerooms() {
      return this.$store.getters.getMyOnerooms(true);
    },
    filteredData() {
      const others = this.$store.getters.getMyOnerooms(false);
      if (!others || !others.length) {
        return [];
      }

      const my_d = this.$store.getters.getMyOnerooms(true);

      if (!my_d.length) {
        return others;
      }

      const myCluster = my_d[0].cluster;
      // others 배열에서 cluster 값이 myCluster와 일치하는 항목만 반환
      return others.filter((item) => item.cluster === myCluster);
    },
  },
  methods: {
    formatToLocalTime(dateTimeString) {
      const dateObj = new Date(dateTimeString);
      return dateObj.toLocaleString(); // 현재 브라우저의 로컬 시간으로 변환하여 반환
    },
    settingaddress(lat, lng, title) {
      // Vuex store에 값을 저장합니다.
      this.$nextTick(() => {
        this.$set(this.choose_zickbang, 0, { lat, lng, title });
      });
      this.$nextTick(() => {
        this.$store.commit("SET_ZICKBANG_POINT", this.choose_zickbang);
      });
      this.$store.dispatch("clearAddress");
      this.$nextTick(() => {
        this.reMountPost();
      });
    },
    async reMountPost() {
      try {
        let dataToSend = null;
        console.log(this.myAddress);
        console.log(this.zickbang_point);

        if (
          !this.myAddress &&
          Array.isArray(this.zickbang_point) &&
          this.zickbang_point.length > 0
        ) {
          dataToSend = this.zickbang_point;
        } else if (
          this.myAddress &&
          (!this.zickbang_point ||
            (Array.isArray(this.zickbang_point) &&
              this.zickbang_point.length === 0))
        ) {
          dataToSend = this.myAddress;
        } else {
          throw new Error("주소 또는 zickbang_point가 필요합니다.");
        }

        const res = await axios.post(`http://localhost:8000/api/info`, {
          address: dataToSend,
          selected: JSON.stringify(this.buttons),
        });

        console.log("---axios Post 성공---- /info ");
        const receivedData = res.data;

        const processedData = {
          scorebox: receivedData.scorebox,
          info_store: JSON.parse(receivedData.info_store),
          address_latlng: receivedData.address_point,
          bus: JSON.parse(receivedData.bus_data),
          oneroom_all: JSON.parse(receivedData.oneroom_data_cluster),
          oneroom_data_cluster: receivedData.my_data,
        };

        // Vue 데이터에 할당
        this.thendata = processedData;

        // Vuex 스토어에 데이터 저장
        this.$nextTick(() => {
          this.$store.dispatch("clearRecievedData");
        });
        this.$nextTick(() => {
          this.$store.commit("SET_RECEIVED_DATA_INFO", processedData);
        });
        console.log("postthen vuex 저장 완료");
      } catch (error) {
        console.error(error.message);
        // 에러 메시지 처리 등
      }
    },
  },
};
</script>

<style>
.score {
  font-size: 50px;
  font-weight: bold;
  text-align: center;
  /* 만약 화면 전체를 가운데 정렬하고 싶다면 아래와 같이 사용합니다 */
  /* display: flex;
  justify-content: center;
  align-items: center; */
}
.card-title {
  font-size: 24px;
  font-weight: bold;
  text-align: center;
}
.page-background {
  background-color: #1e1e2f; /* 적용할 배경색 코드를 입력하세요 */
  width: 100%;
  height: 100%;
  /* 기타 스타일링을 원하면 여기에 추가하세요 */
}
/* 추가적인 스타일링을 위한 클래스 또는 선택자를 여기에 추가하세요 */
</style>

프로젝트를 마무리하며
이 프로젝트를 한 이유는 간단하다. 인생 신념이 원래 배웠던거 또 공부하는 것을 싫어한다. 반복훈련보단 새로운 것을 공부해야 한다. 대입때 현우진쌤의 조언이었던 것 같다. 다들 번지르르하게, 딥러닝 모델 때려넣고 하이-퀄리티 기술인 것 처럼 포장한다. 현업에서 딥러닝이 적용되는 비율은 그리 높지않다. 일단 딥러닝에 합당한 만큼의 데이터가 확보되어있지 않은 경우가 대부분이다. 또한 미리 잘 정제된 데이터에는 적용될 아이디어가 한정적이다. 대부분의 학생들이 좋은 데이터에 기획을 맞춘다. 이게 무슨 짓인지 사실 잘 모르겠다. 좋은 데이터는 예쁜 결과를 가져온다. 자신의 아이디어를 몇 개 열거해놓고 이 아이디어에 맞는 데이터를 무료 혹은 적은 비용으로 구할 수 있는가?(개인 프로젝트에 데이터 비용으로 몇 백만원을 들일 수는 없으니까.) 결국 데이터를 직접 구하면 결과가 예쁘게 나타나지는 않는다. 나한테 필요한 칼럼이 내가 캐온 과정에는 없는 경우가 허다하고, 어디서 새로 구한것을 맞춰서 데이터를 합치려면 합치는대로 문제가 발생한다. 데이터를 구하는 것에 대한 정의는 각각이 다르다. 누군가는 캐글에서 잘 정제된 대회 데이터로, 누군가는 특정 서비스API로 받아서, 누군가는 직접 발로뛰며 기록한다. 자신의 기획에 맞다면 무엇이든 상관없다고 생각한다. 잘 정제된 데이터도 자신의 기획에서 우연히 쓰기 좋은 상황이라면 운이 좋게 사용할 수 있는 것이라고 생각한다. 나는 웹 지식도 매우 부족했고, 이 모든 것은 이번 학기에 시작한 공부와 프로젝트이다. 굴러가게 하는 것이 목적이다보니 여전히 세부적으로 여전히 미숙한 구조이다. 나름의 풀스택 개발을 해보면서 느낀 것은 결국은 코딩감각과 코딩전 설계가 중요했다. 프로젝트 실행 절차적인 부분을 잘 관리해야한다는 생각이 들었다. chat-gpt가 상용화 된 시점에서 chat-gpt로 부터 받는 코드들은 유용하지만서도 확인과 다듬기가 필요하다. 그리고 코드를 어떻게 짜든 처음에 잘 설계를 하지 않는다면 하나의 메소드만 작성하면 될 것을 3~4개 작성해서 스크립트가 더러워진다. chat-gpt는 프로젝트 진행의 속도만 올려줄 뿐이다. 그 안의 퀄리티는 프로그래머 본인의 역량이라고 생각한다. 또한 내가 어디에 흥미를 느끼는지 알 수 있었다. 프론트의 경우 분명히 빠른 보상이 온다. 하지만 기획적인 감각과 유저로 생각해보는 등 꽤 머리아픈 지점이 많았다. 백엔드나 DB구성을 하면서 느낀 것은 훨씬 수학적인 요구가 많으며, 어려우면서도 억울한 느낌이었다.(억울한 작업이 내 취향인거 같기도 하고) 보이는게 없으니 뿌듯함을 느끼는 것이 쉽지 않았는데, 본질을 구성하고 있다고 생각했다. 잘 구성된 백엔드가 프론트의 확장력을 만들어준다고 생각했다. 사실 결론적으로 더 어려웠던 것은 프론트였다. 내가 HTML, CSS, JS에 대해 미숙했기 때문이고 그 때문에 강의의 도움을 많이 받았다. 이것은 기술적 문제임으로 금방 해결될 문제라고 생각했다. 결국 더 어려운 것은 백엔드구나라는 것을 느꼈고, 지금 이정도 프로젝트에도 통신시간이 꽤 걸리는 부분이 존재했다. 이런 것들을 개선하는 것이 얼마나 힘든일인지, 많은 이론적 기술을 배워야 가능하겠구나 생각했다. 자료구조나 알고리즘의 필요성을 많이 느낀 것 같다. 아직오 어떻게 짜야 확장성있게 여지를 남겨두면서 코드를 짤 수 있는지등 어디서 객체화를 해야하며 등등 감각이 없는 부분이 많다. 많은 대학생들이 나는 프론트가 취향이야, 백엔드가 취향이야, 데이터 엔지니어가 취향이야 등 세부진로를 결정하는데 있어서 기본적으로 미숙한(?) 풀스택 프로젝트를 해보았으면 한다. 하여튼 힘든 프로젝트였는데 누군가는 용두사미라고 할지언정 나에게는 배움이 많은 프로젝트였다. 현업에서 더 구르면서 배워보도록 하자:)

profile
자바집사의 거북이 수련법

0개의 댓글