[SKN FAMILY AI CAMP]/주간

🐉 SKN FAMILY AI CAMP 13기 25주차 후기 (2025.09.08 ~ 2025.09.15)

ki-june 2025. 9. 17. 16:03

 

📍 간단 후기

 

 

🏷️ 최종 프로젝트

 

최종 프로젝트가 종료됐다. 결론부터 얘기하면 잘 끝났다. 1등을 했기 때문이다!

저번주까지만 해도 마무리해야 될 작업들이 많아서 걱정됐었다. 그래도 최대한 긍정적으로 생각하면서 프로젝트를 끝까지 이끌어가려고 하다 보니, 좋은 결과가 있었다고 생각한다. 할 수 있다는 말을 프로젝트하면서 팀원들끼리 가장 많이 말한 것 같다. 나열해 보자면 이렇다.

  • 할 수 있다.
  • 거의 다 했어.
  • 완성이야.
  • .....

우리 팀원들은 대체적으로 긍정적이었다. 물론 힘들어서 안될 것 같아 보일 때도 있었지만, 결국엔 다들 긍정적으로 생각했다.

사실 위 말을 하면서 진짜 할 수 있었거나, 거의 다 했다거나, 완성이었던 적은...... 거의 없었다. 그렇지만 계속 저런 말을 하면서 본인도, 팀원들도 믿게 만드는 그 마인드가 결국 마지막에 빛을 발한 거라고 생각한다.

 

 


 

 

📍 좋았던 점

 

 

  • 웹 크롤링

 

 

 

의아해 하실 수도 있다. 크롤링을 마지막 주차에? 그것도 좋았던 점으로?? 그렇지만 사실이다. 우리의 플랫폼이 연동되었던 것도 좋았다. 그러나 난 이 결과도 너무 좋았다.

아마 다들 그런 경험이 있을 것이다. 계에 에에 속 안되던 걸 성공했을 때. 위 크롤링이 그랬다. 결국 성공했다.

 

기존에는 별점 데이터를 뺀, 차량 별 후기 데이터와 tags만 뽑아서 json 화했었다. 그러나 우리 페이지 특성상 별점 데이터가 있어야 사용자들로 하여금 Insight를 도출해 낼 수 있었기에, 별점 데이터 크롤링을 추가로 해야 될 필요가 있었다. 그러나 별점 데이터 크롤링이 생각보다 힘들었다. 전부 5.0으로 계산되었기 때문이었다. 그래서 처음에는 별점 데이터가 있는 칸의 클래스를 찾아서 stat-half와 star-on이 적혀있는 클래스를 계산했다. 그러나, 역시나 예상대로 되지 않았다. star-half * 0.5를 해주었지만, star-on만 어쩜 그리 쏙쏙 잘 뽑아내는지, 정말 이해가 가질 않았다. 그래서 파싱도 3번에 걸쳐서 진행했었다.

 

해결책은 다음과 같았다.

 

# ---- 공통 파서 (한 컨테이너에서 점수 읽기) ----
def parse_rating_from_container(root):
    def pick_num(text):
        if not text: return None
        m = re.search(r'([0-5](?:\.[05])?)', str(text))
        return float(m.group(1)) if m else None

    # 1) 별 컨테이너 고정
    star_root = None
    for sel in [".summary-wrap .review-star", ".review-star", "[class*='review-star']"]:
        try:
            star_root = root.find_element(By.CSS_SELECTOR, sel)
            break
        except:
            continue
    if not star_root:
        return None, "no_star_root"

    # 2) aria-label
    try:
        aria = star_root.get_attribute("aria-label") or ""
        val = pick_num(aria)
        if val is not None:
            return max(0.0, min(5.0, val)), "aria-label"
    except:
        pass

    # 3) 숨김텍스트
    try:
        hidden_nodes = []
        for sel in [".blind", ".sr-only", ".visually-hidden", ".ir", "[aria-hidden='false']"]:
            hidden_nodes += star_root.find_elements(By.CSS_SELECTOR, sel)
        for node in hidden_nodes:
            val = pick_num(node.text)
            if val is not None:
                return max(0.0, min(5.0, val)), "hidden-text"
    except:
        pass

    # 4) CSS 변수
    try:
        styles = driver.execute_script("return getComputedStyle(arguments[0]);", star_root)
        # Selenium에서 getPropertyValue를 직접 못 부르니 자바스크립트로 가져오자
        for var in ["--rating", "--score", "--value", "--rate"]:
            val = driver.execute_script("return getComputedStyle(arguments[0]).getPropertyValue(arguments[1]);", star_root, var)
            if val:
                num = pick_num(val.strip())
                if num is not None:
                    return max(0.0, min(5.0, num)), f"css-var({var})"
    except:
        pass

    # 5) 게이지 width
    try:
        bar = star_root.find_element(By.CSS_SELECTOR, ".bar")
        pct = None
        style_attr = bar.get_attribute("style") or ""
        m = re.search(r'width\s*:\s*([0-9.]+)\s*%', style_attr)
        if m:
            pct = float(m.group(1))
        else:
            # 계산된 width / 컨테이너 width로 비율
            bw = driver.execute_script("return parseFloat(getComputedStyle(arguments[0]).width);", bar)
            cw = driver.execute_script("return parseFloat(getComputedStyle(arguments[0]).width);", star_root)
            if bw and cw and cw > 0:
                pct = (bw / cw) * 100.0
        if pct is not None:
            val = round(min(5.0, max(0.0, pct / 20.0)) * 2) / 2.0
            return val, "bar-width"
    except:
        pass

    # 6) 클래스에 rating-xx
    try:
        cls = (star_root.get_attribute("class") or "") + " " + (root.get_attribute("class") or "")
        m = re.search(r'rating[-_]?([0-9]{2})', cls)
        if m:
            val = int(m.group(1)) / 10.0
            return max(0.0, min(5.0, val)), "class-rating"
    except:
        pass

    # 7) 아이콘 카운트(최후)
    try:
        full = len(star_root.find_elements(By.CSS_SELECTOR, ".star-on"))
        half = len(star_root.find_elements(By.CSS_SELECTOR, ".star-half"))
        # star-off는 점수에 직접 더하지 않음
        if full or half:
            return max(0.0, min(5.0, full + 0.5 * half)), "icon-count"
    except:
        pass

    return None, "no-match"

 

 

코드는 되게 복잡해 보인다. 그러나 결론부터 얘기하자면, 범용 파서를 사용했다. 내용은 다음과 같다.

 

  1. arial-label : "별 4.5개"와 같은 문구를 읽어 점수 추출
  2. 숨김 텍스트에서 읽기 : 시각적으로 숨겨져 있으나 접근성용으로 남겨둔 텍스트(.sr-only, .blind 등)에서 별점 찾음 / 성공 시 "hidden-text" 경로 반환.
  3. CSS 변수 확인 : 별점이 CSS custom property (--rating: 4.0) 형태로 있을 경우 읽어옴 / 성공 시 "css-var(--rating)" 같은 출처 반환.
  4. 게이지 bar width 계산 : 예: width 80% → 별점 4.0 / 반올림은 0.5 단위까지. / 성공 시 "bar-width" 반환.
  5. 클래스 명에서 추출 : star-on인지, star-half인지, 등
  6. 아이콘 카운트 : <i class="star-on"> 갯수 세기 방식 / 꽉 찬 별은 1점, 반 별은 0.5점 더해서 계산 / 성공 시 "icon-count" 반환

 

이렇게 많은 파서를 통해 결국 성공해 낼 수 있어서 정말 뿌듯했다.

 

 

  • Airflow 자동화

 

 

위 내용과 이어지긴 하지만 리뷰 데이터, 즉 별점 데이터까지 크롤링한 데이터는 Airflow를 통해 자동으로 업로드되도록 하였다. 매일 자정 1페이지씩 말이다. 이를 위해 수동으로 잘 진행되고 있나 확인해 보았다. 결과는 위 그림과 같이 6번이나 실패했다. 원인을 분석한 결과, 원인은 아래와 같은 부분에 있었다.

# 2. DB 데이터 임포트 (리뷰 + 집계)
import_data_task = BashOperator(
task_id="import_car_data",
bash_command="docker exec babsim_django_gunicorn_1 python manage.py import_data",
)

 

바로 bash_command 변수이다. 처음에는 docker-compose exec 이라는 명령어로 변수를 저장했다. 그러니까 Airflow Docker 컨테이너에서는 해당 명령어를 실행하지 못한다는 것이었다. 해당 모듈이 설치되어 있지 않았고, 그에 따라서 명령어 실행에 오류가 생긴 것이었다. 그래서 위와 같이 명령어를 바꿔주니, 곧바로 7번째 만에 성공하는 것을 볼 수 있었다.

docker-compose 명령어 뿐 아니라, babsim_django_gunicorn_1이라는 부분도 오류가 났었다. 컨테이너명과 실행할 때 나오는 이름이 달랐기에 오류가 났던 것이었다. 결국 컨테이너 명에 맞춰서 실행하는 것이 맞았다.

 

 

 

  • 도메인 설정

 

 

Amazon Route 53으로 도메인을 사려고 했다. 위 그림은 도메인 설정하는 과정이다. 도메인을 사는 이유는 항상 고정된, 할당된 IP만으로 사용해 왔기에, 우리의 고유 프로젝트 이름을 통한 도메인 이름을 정하고 싶기 때문이었다. 또 다른 이유는 '멋'이다. 사실 11.111.1111 뭐 이런 식의 IP로만 들어가다 보니, 색다르게 표현하고 싶었다. 또 다른 웹 페이지들이나 플랫폼들도 각자만의 도메인이 있었다. 그래서 클라우드 베포 업무를 담당했기에 도메인을 정하고 베포 하는 방법을 채택해 봤다.

 

 

위 그림은 도메인을 사고 성공한 화면이다. 이게 뭐라고 상당히 뿌듯했다. 이를 Django Framework에 적용시키기 위해서는, 다들 프로젝트 구조가 다르겠지만. env나 settings.py 등에 있는 IP 주소를 해당 도메인 이름으로 바꿔야 할 것이다. 또 우리 프로젝트는 구글 OAuth를 사용하기 있었기 때문에 Redirection 경로를 추가해 주는 작업도 병행했다.

 

도메인을 사용하고 싶다고 해서 신청하고 도메인 이름을 얻는 것이 아닌, 도메인을 해당 프로젝트에 적용시키기 위해서는 또 다른 설정을 별개로 해줘야 한다는 점에서 새로운 것을 배운 느낌이었다. 아니, 배웠다. 그렇게 완성하고 나서 해당 도메인으로 들어갔을 때 얻은 뿌듯함은 괜히 프로젝트가 성공했다는 뿌듯함으로 이어졌다.

 

 


 

 

📍 부족한 점

 

 

  • 프로젝트의 완성도

 

 

사실 우리가 의도했던 전개는 사용자에 맞춰 이미지가 나오도록 설정하는 것이었다. 물론 이미지가 잘 뽑히고, 사용자에게 잘 보인다. 그러나 지금 위 그림과 같은 이미지만 중복해서 생성하는 것을 확인할 수 있었다. 하지만, 이 과정에서도 희망은 있었다.

 

 

우리는 멀티모달 모델인 FLUX.1을 사용했다. 이 모델은 이미지의 수정도 가능했다. 위 그림은 로고가 2개였던 이미지를 1개로 바꿔주고, 색상도 바꿔준 모습이다. 진짜 잘 나오는 점을 확인할 수 있다.

 

첫 번째 사진의 오른쪽 아래를 보면 알 수 있듯이, 우리 플랫폼은 체크리스트가 존재한다. 그리고 사용자와 대화하면서 해당 체크리스트의 게이지도 올라간다. 완벽하게 잘 작동했다. 또 해당 부분에 마우스를 갖다 대면 체크리스트의 어떤 부분이 체크됐는지 자동으로 반영되게 하여 사용자가 직접 눈으로 확인도 가능했다.

 

또 완성도는 아니지만, 부족한 점은 다른 페이지들과 챗봇 페이지의 연동을 제대로 못했다는 점이다. 사용자가 차량 별 Insight를 확인할 수 있는 페이지와 챗봇이 연동되었어도 더 완벽한 구성의 프로젝트가 되지 않았을까 하는 아쉬움도 들었다.

 

 

 


 

 

📍 성찰 및 마무리

 

 

모든 팀들이 그렇겠지만, 프로젝트의 완성도가 100% 일 것이지는 않을 것이다. 다만, "더 잘할 수 있었을 텐데" 하는 아쉬움만 남기에는 여태껏 해왔던 내용들이 헛수고가 된다. 그래서 나는 그렇게 생각한다. 2달 동안 어떤 주제를 하느냐에 따라 시간이 부족하기도 하고, 여유가 있기도 했을 것이다. 그러나 최종 프로젝트이고, 원하는 기술을 써보고 싶었을 것이고, 마지막으로 그것을 해내고 싶어 했다면 우리의 프로젝트 완성도는 100%이다.

 

Github 링크

https://github.com/SKNETWORKS-FAMILY-AICAMP/SKN13-FINAL-3TEAM

 

GitHub - SKNETWORKS-FAMILY-AICAMP/SKN13-FINAL-3TEAM

Contribute to SKNETWORKS-FAMILY-AICAMP/SKN13-FINAL-3TEAM development by creating an account on GitHub.

github.com

 

 

우리 조는 크게 4가지 기술을 활용했다. 일반적인 LLM이 아닌 기업특화 sLLM, 기업특화 멀티모달, 이미지 생성을 통한 3D 이미지 및 영상 생성이다. 이것을 우리는 "멀티모달의 확장"이라고 부른다. 기업 특화에 맞추어 프로젝트를 진행했기에 사용자 친화적인 챗봇으로도 활용하고 싶었다. 이를 위해 우리는 "Human In The Loop"를 채택한 것이다. 이는 사용자가 원하는 것에 맞춰 이미지를 생성하기 때문에, 아무래도 더 기술성이 높은 프로젝트였지 않았나 싶다. 그래서 2달이 짧게 느껴졌기도 했고, 이걸 해낸 우리 조에게도 아주 대단하다고 말해주고 싶다.

 

이렇게 부트캠프도 종료되었다. 앞으로 어떤 기업에서 어떤 일을 하고 있을지는 잘 모르겠지만, 적어도 부트캠프에 들어오기 전과 비교해 봤을 때보다 저 자신감 있게 지원할 수 있을 것 같다. 이 글을 읽는 여러분들도 아마 부트캠프 내용이 궁금하신 분들도 많을 것이라 생각한다. 이 글을 참고해서 지원할지 말지도 고민하는 분들도 있을 것이다. 물론 본인의 선택에 의해 결정되어야 한다. 그러나 한 가지 말해주고 싶은 점이 있다. 본인이 어떤 프로젝트를 하고 싶은지 선택하라는 점이다. 원하는 것을 하기 위해 어떤 기술을 써야 하는지도 파악해 볼 필요가 있다. 팀 프로젝트이다 보니, 원하지 않는 주제가 선정될 수도 있다. 그래도 괜찮다. 본인이 사용하고 싶은 기술을 어떻게 사용할지 팀원들에게 얘기해 본다면 충분히 원하는 대로 할 수 있고, 본인의 역량을 발휘할 수 있을 것이다.

 

앞으로 모두들 원하는 기업에 들어가서 하고 싶은 일을 하며 행복하게 살아가길 바라며 글을 마무리하겠다.

 

취업하자 취업하자 취뽀하자!!