diff --git a/apps/kokomen-native/ios/kokomennative/Info.plist b/apps/kokomen-native/ios/kokomennative/Info.plist index c75bfe99..4cb7749c 100644 --- a/apps/kokomen-native/ios/kokomennative/Info.plist +++ b/apps/kokomen-native/ios/kokomennative/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.0 + 0.0.1 CFBundleSignature ???? CFBundleURLTypes @@ -48,6 +48,8 @@ $(PRODUCT_NAME)이 음성 면접 서비스 제공을 위해 마이크 권한이 필요합니다. NSSpeechRecognitionUsageDescription $(PRODUCT_NAME)이 음성 면접 과정에서 음성 인식 권한이 필요합니다. + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME)에서 사진을 업로드하거나 프로필 이미지를 변경할 때 사진 라이브러리 접근이 필요합니다. UILaunchStoryboardName SplashScreen UIRequiredDeviceCapabilities @@ -57,7 +59,7 @@ UIRequiresFullScreen UIStatusBarStyle - + UIStatusBarStyleDefault UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/apps/kokomen-server/compose.dev.yml b/apps/kokomen-server/compose.dev.yml new file mode 100644 index 00000000..d8ac0388 --- /dev/null +++ b/apps/kokomen-server/compose.dev.yml @@ -0,0 +1,28 @@ +services: + kokomen-nest-server: + build: + context: . + dockerfile: Dockerfile + container_name: kokomen-nest-server + environment: + - NODE_ENV=development + networks: + - dev-kokomen-net + depends_on: + - kokomen-local-redis + nginx: + image: nginx:alpine + container_name: kokomen-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - dev-kokomen-net + depends_on: + - kokomen-nest-server + +networks: + dev-kokomen-net: + driver: bridge diff --git a/apps/kokomen-server/nginx.conf b/apps/kokomen-server/nginx.conf new file mode 100644 index 00000000..12894a09 --- /dev/null +++ b/apps/kokomen-server/nginx.conf @@ -0,0 +1,30 @@ +events { + worker_connections 1024; +} + +http { + upstream backend { + server kokomen-nest-server:3000; + } + + server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /graphql { + proxy_pass http://backend/graphql; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} \ No newline at end of file diff --git a/apps/kokomen-server/src/app.module.ts b/apps/kokomen-server/src/app.module.ts index fc9a0764..5c4359e6 100644 --- a/apps/kokomen-server/src/app.module.ts +++ b/apps/kokomen-server/src/app.module.ts @@ -10,13 +10,14 @@ import { Member } from "./member/domains/member"; import { MemberResolver } from "./member/member.resolver"; import { MemberService } from "./member/member.service"; import { RedisModule } from "src/redis/redis.module"; -import { TestResolver } from "src/test.resolver"; +import { CategoryModule } from "src/interview/modules/category"; +import { RootQuestionModule } from "src/interview/modules/rootQuestion"; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, - envFilePath: [`.env.${process.env.NODE_ENV || "development"}`, ".env"], + envFilePath: [`env.${process.env.NODE_ENV || "development"}.`, ".env"], load: [appConfig] }), TypeOrmModule.forRoot({ @@ -26,18 +27,19 @@ import { TestResolver } from "src/test.resolver"; username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, - entities: [__dirname + "/**/domains/*.{ts,js}"], - synchronize: true + entities: [__dirname + "/**/domains/*.{ts,js}"] }), TypeOrmModule.forFeature([Member]), GraphQLModule.forRoot({ driver: ApolloDriver, - playground: true, + graphiql: process.env.NODE_ENV === "development", autoSchemaFile: true }), - RedisModule + RedisModule, + CategoryModule, + RootQuestionModule ], controllers: [AppController], - providers: [AppService, MemberResolver, MemberService, TestResolver] + providers: [AppService, MemberResolver, MemberService] }) export class AppModule {} diff --git a/apps/kokomen-server/src/config/db.config.ts b/apps/kokomen-server/src/config/db.config.ts index 9b8ae0a8..b8c34efb 100644 --- a/apps/kokomen-server/src/config/db.config.ts +++ b/apps/kokomen-server/src/config/db.config.ts @@ -3,7 +3,7 @@ import { registerAs } from "@nestjs/config"; export default registerAs("db", () => ({ type: "mysql", host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT, 10), + port: parseInt(process.env.DB_PORT || "3306", 10), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE diff --git a/apps/kokomen-server/src/interview/domains/category.ts b/apps/kokomen-server/src/interview/domains/category.ts new file mode 100644 index 00000000..7549f288 --- /dev/null +++ b/apps/kokomen-server/src/interview/domains/category.ts @@ -0,0 +1,179 @@ +import { ObjectType, Field, registerEnumType } from "@nestjs/graphql"; + +export enum CategoryType { + ALGORITHM_DATA_STRUCTURE = "ALGORITHM_DATA_STRUCTURE", + DATABASE = "DATABASE", + NETWORK = "NETWORK", + OPERATING_SYSTEM = "OPERATING_SYSTEM", + JAVA_SPRING = "JAVA_SPRING", + INFRA = "INFRA", + FRONTEND = "FRONTEND", + REACT = "REACT", + JAVASCRIPT_TYPESCRIPT = "JAVASCRIPT_TYPESCRIPT" +} + +registerEnumType(CategoryType, { + name: "CategoryType" +}); + +const CATEGORY_DATA: Record< + CategoryType, + { title: string; description: string; imageUrl: string } +> = { + [CategoryType.ALGORITHM_DATA_STRUCTURE]: { + title: "알고리즘/자료구조", + description: ` + 알고리즘과 자료구조는 문제를 효율적으로 해결하기 위한 핵심 +요소입니다. + 자료구조는 데이터를 저장하고 관리하는 방법을 정의하며, +배열·리스트·스택·큐·트리·그래프 등 다양한 구조를 통해 데이터를 +체계적으로 다룰 수 있습니다. + 알고리즘은 이러한 자료구조를 기반으로 문제를 해결하는 단계별 + 절차를 의미하며, 정렬·탐색·그래프 탐색·동적 프로그래밍 등 다양한 +기법이 존재합니다. + 적절한 자료구조와 알고리즘을 선택하고 구현하는 것은 +프로그램의 성능과 효율성을 결정짓는 중요한 요소입니다. + `, + imageUrl: "kokomen-algorithm-data-structure.png" + }, + [CategoryType.DATABASE]: { + title: "데이터베이스", + description: ` + 데이터베이스는 대량의 데이터를 효율적으로 저장하고 관리하는 +시스템입니다. + 관계형 데이터베이스(RDBMS)는 테이블 구조와 SQL을 기반으로 +데이터를 관리하고, + NoSQL은 비정형 데이터와 대규모 분산 처리를 지원합니다. + 적절한 인덱스 설계는 빠른 데이터 조회와 시스템 성능에 중요한 + 영향을 미칩니다. + `, + imageUrl: "kokomen-database-v2.png" + }, + [CategoryType.NETWORK]: { + title: "네트워크", + description: ` + 네트워크는 여러 컴퓨터와 시스템이 서로 데이터를 주고받을 수 +있도록 구성된 통신 구조입니다. + 네트워크 계층 구조(OSI 7계층, TCP/IP 4계층), 프로토콜(TCP, +UDP, HTTP 등), + 라우팅, 패킷 전송 방식 등 다양한 개념을 이해하는 것이 +네트워크의 핵심입니다. + `, + imageUrl: "kokomen-network-v2.png" + }, + [CategoryType.OPERATING_SYSTEM]: { + title: "운영체제", + description: ` + 운영체제는 하드웨어와 소프트웨어 자원을 효율적으로 관리하고, + 사용자와 응용 프로그램이 시스템을 효과적으로 사용할 수 있도록 +지원하는 핵심 소프트웨어입니다. + 프로세스 및 스레드 관리, 메모리 관리, 파일 시스템, +입출력(I/O) 제어, 그리고 CPU 스케줄링과 같은 기능을 담당합니다. + OS의 구조와 동작 원리를 이해하는 것은 시스템 개발 및 +최적화의 기초가 됩니다. + `, + imageUrl: "kokomen-operating-system-v2.png" + }, + [CategoryType.JAVA_SPRING]: { + title: "자바/스프링", + description: ` + 자바와 스프링은 안정적이고 확장성 있는 백엔드 개발을 위한 +핵심 기술입니다. + 자바는 강력한 객체지향 언어로, 대규모 시스템에서도 일관성과 +유지보수성을 보장합니다. + 스프링은 의존성 주입, 트랜잭션 관리, 보안, 데이터 접근 등 +백엔드 개발에 필수적인 기능을 제공하여 복잡한 애플리케이션을 +효율적으로 구현할 수 있게 합니다. + 특히 스프링 부트를 활용하면 설정과 배포가 간소화되어, 빠르게 + 안정적인 서비스를 개발하고 운영할 수 있습니다. + `, + imageUrl: "kokomen-java-spring.png" + }, + [CategoryType.INFRA]: { + title: "인프라", + description: ` + 인프라는 안정적이고 확장 가능한 서비스를 구축하기 위해 +필수적인 기반 기술을 의미합니다. + 데이터베이스는 서비스의 핵심 데이터를 안전하게 저장하고, +Redis와 같은 인메모리 캐시는 + 빠른 응답 속도를 보장합니다. Kafka와 같은 메시지 큐는 대규모 + 트래픽 환경에서도 안정적인 + 비동기 처리를 가능하게 합니다. 이러한 인프라 기술들은 눈에 +잘 드러나지는 않지만, + 대규모 서비스의 안정성과 성능을 지탱하는 든든한 토대가 되며, + 백엔드 개발자가 반드시 + 이해하고 다뤄야 할 영역입니다. + `, + imageUrl: "kokomen-infra.png" + }, + [CategoryType.FRONTEND]: { + title: "프론트엔드", + description: ` + 프론트엔드 전반적인 지식에 대한 문제가 출제됩니다. 특히, +프론트엔드 개발에서 필요한 실무적 지식이나 브라우저에 관한 심도 깊은 + 질문들이 출제됩니다. + `, + imageUrl: "kokomen-frontend.png" + }, + [CategoryType.REACT]: { + title: "리액트", + description: ` + 리액트는 사용자 인터페이스를 만들기 위한 자바스크립트 +라이브러리입니다. + 리액트에 관한 지식들을 중심으로 출제됩니다. 특히, 리액트에서 + 사용되는 API나 리액트 내에서 사용되는 주요 기술들이 출제됩니다. + `, + imageUrl: "kokomen-react.png" + }, + [CategoryType.JAVASCRIPT_TYPESCRIPT]: { + title: "자바스크립트/타입스크립트", + description: ` + 자바스크립트는 웹 브라우저와 서버에서 실행되는 동적 +프로그래밍 언어로, 현대 웹 개발의 핵심 기술입니다. + 주로 자바스크립트의 언어에 대한 이해도를 묻는 질문과 +자바스크립트를 동작시키는 엔진, 추가적으로 정적 분석을 위한 +타입스크립트에 대한 질문 또한 일부 출제됩니다. + `, + imageUrl: "kokomen-javascript-typescript.png" + } +}; + +@ObjectType() +export class Category { + @Field(() => CategoryType) + type: CategoryType; + + @Field() + title: string; + + @Field() + description: string; + + @Field() + imageUrl: string; + + constructor( + type: CategoryType, + title: string, + description: string, + imageUrl: string + ) { + this.type = type; + this.title = title; + this.description = description; + this.imageUrl = imageUrl; + } + + static getCategories(): Category[] { + const BASE_URL = process.env.CLOUD_FRONT_DOMAIN_URL + "category-image/"; + return Object.values(CategoryType).map((type: CategoryType) => { + const data = CATEGORY_DATA[type]; + return new Category( + type, + data.title, + data.description, + BASE_URL + data.imageUrl + ); + }); + } +} diff --git a/apps/kokomen-server/src/interview/domains/rootQuestion.ts b/apps/kokomen-server/src/interview/domains/rootQuestion.ts index 9563877d..ab262f06 100644 --- a/apps/kokomen-server/src/interview/domains/rootQuestion.ts +++ b/apps/kokomen-server/src/interview/domains/rootQuestion.ts @@ -7,22 +7,7 @@ import { } from "typeorm"; import { Interview } from "./interview"; import { Field, ID, Int, ObjectType, registerEnumType } from "@nestjs/graphql"; - -export enum QuestionCategory { - ALGORITHM_DATA_STRUCTURE, - DATABASE, - NETWORK, - OPERATING_SYSTEM, - JAVA_SPRING, - INFRA, - FRONTEND, - REACT, - JAVASCRIPT_TYPESCRIPT -} - -registerEnumType(QuestionCategory, { - name: "QuestionCategory" -}); +import { CategoryType } from "src/interview/domains/category"; export enum QuestionState { ACTIVE, @@ -48,9 +33,9 @@ export class RootQuestion { @Column({ type: "varchar", length: 1000 }) content: string; - @Field(() => QuestionCategory) - @Column({ type: "enum", enum: QuestionCategory }) - category: QuestionCategory; + @Field(() => CategoryType) + @Column({ type: "enum", enum: CategoryType }) + category: CategoryType; @Field(() => QuestionState) @Column({ type: "enum", enum: QuestionState }) diff --git a/apps/kokomen-server/src/interview/modules/category.ts b/apps/kokomen-server/src/interview/modules/category.ts new file mode 100644 index 00000000..f664b8f1 --- /dev/null +++ b/apps/kokomen-server/src/interview/modules/category.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { CategoryResolver } from "src/interview/resolvers/category"; +import { CategoryService } from "src/interview/services/category"; + +@Module({ + providers: [CategoryService, CategoryResolver] +}) +export class CategoryModule {} diff --git a/apps/kokomen-server/src/interview/modules/rootQuestion.ts b/apps/kokomen-server/src/interview/modules/rootQuestion.ts new file mode 100644 index 00000000..e89530aa --- /dev/null +++ b/apps/kokomen-server/src/interview/modules/rootQuestion.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { RootQuestion } from "src/interview/domains/rootQuestion"; +import { RootQuestionResolver } from "src/interview/resolvers/rootQuestion"; +import { RootQuestionService } from "src/interview/services/rootQuestion"; + +@Module({ + imports: [TypeOrmModule.forFeature([RootQuestion])], + providers: [RootQuestionService, RootQuestionResolver] +}) +export class RootQuestionModule {} diff --git a/apps/kokomen-server/src/interview/resolvers/category.ts b/apps/kokomen-server/src/interview/resolvers/category.ts new file mode 100644 index 00000000..1a7efc78 --- /dev/null +++ b/apps/kokomen-server/src/interview/resolvers/category.ts @@ -0,0 +1,20 @@ +import { Resolver, Query, Args } from "@nestjs/graphql"; +import { Category, CategoryType } from "src/interview/domains/category"; +import { CategoryService } from "src/interview/services/category"; + +@Resolver(() => Category) +export class CategoryResolver { + constructor(private readonly categoryService: CategoryService) {} + + @Query(() => [Category]) + async categories(): Promise { + return this.categoryService.findAll(); + } + + @Query(() => Category, { nullable: true }) + async category( + @Args("type", { type: () => CategoryType }) type: CategoryType + ): Promise { + return this.categoryService.findOne(type); + } +} diff --git a/apps/kokomen-server/src/interview/resolvers/rootQuestion.ts b/apps/kokomen-server/src/interview/resolvers/rootQuestion.ts new file mode 100644 index 00000000..5d09dcd5 --- /dev/null +++ b/apps/kokomen-server/src/interview/resolvers/rootQuestion.ts @@ -0,0 +1,21 @@ +import { Args, Query, Resolver } from "@nestjs/graphql"; +import { CategoryType } from "src/interview/domains/category"; +import { RootQuestion } from "src/interview/domains/rootQuestion"; +import { RootQuestionService } from "src/interview/services/rootQuestion"; + +@Resolver(() => RootQuestion) +export class RootQuestionResolver { + constructor(private readonly rootQuestionService: RootQuestionService) {} + + @Query(() => [RootQuestion]) + async rootQuestions(): Promise { + return this.rootQuestionService.findAll(); + } + + @Query(() => [RootQuestion]) + async rootQuestionByCategory( + @Args("category", { type: () => CategoryType }) category: CategoryType + ): Promise { + return this.rootQuestionService.findByCategory(category); + } +} diff --git a/apps/kokomen-server/src/interview/services/category.ts b/apps/kokomen-server/src/interview/services/category.ts new file mode 100644 index 00000000..38819c2a --- /dev/null +++ b/apps/kokomen-server/src/interview/services/category.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common"; +import { Category, CategoryType } from "../domains/category"; + +@Injectable() +export class CategoryService { + private readonly categories: Category[]; + constructor() { + this.categories = Category.getCategories(); + } + + async findAll(): Promise { + return this.categories; + } + + async findOne(type: CategoryType): Promise { + const categories = Category.getCategories(); + return categories.find((category) => category.type === type) || null; + } +} diff --git a/apps/kokomen-server/src/interview/services/rootQuestion.ts b/apps/kokomen-server/src/interview/services/rootQuestion.ts new file mode 100644 index 00000000..a55d2e8d --- /dev/null +++ b/apps/kokomen-server/src/interview/services/rootQuestion.ts @@ -0,0 +1,20 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { CategoryType } from "src/interview/domains/category"; +import { RootQuestion } from "src/interview/domains/rootQuestion"; +import { Repository } from "typeorm"; + +@Injectable() +export class RootQuestionService { + constructor( + @InjectRepository(RootQuestion) + private readonly rootQuestionRepository: Repository + ) {} + findAll(): Promise { + return this.rootQuestionRepository.find(); + } + + findByCategory(category: CategoryType): Promise { + return this.rootQuestionRepository.find({ where: { category } }); + } +} diff --git a/apps/kokomen-server/src/main.ts b/apps/kokomen-server/src/main.ts index 4e54cd02..208deb7f 100644 --- a/apps/kokomen-server/src/main.ts +++ b/apps/kokomen-server/src/main.ts @@ -4,15 +4,14 @@ import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); - // CORS 설정 app.enableCors({ origin: [ "http://localhost:3000", "http://localhost:3001", "http://local.kokomen.kr:3000", "https://dev.kokomen.kr" - ], // 클라이언트 도메인 - credentials: true, // 쿠키 포함 허용 + ], + credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization", "Cookie"] });