C & C++/디딤돌 C언어

Part 29. 도서 관리 프로그램 I - 시나리오 및 기본 메뉴 구현

언휴 2024. 1. 22. 13:53

Part 29. 도서 관리 프로그램 I  - 시나리오 및 기본 메뉴 구현

101. 도서 관리 프로그램 실습 시나리오

도서 관리 프로그램 시나리오 - C언어

전산 기술은 빠르게 발전하고 새로운 기술과 개발 공정이 나오고 있어요.
여기에서는 시나리오를 소개한 후에 요구 분석하고 설계한 후에 구현하는 공정으로 진행할게요.

요구 분석에서는 프로그램의 외부 요소와 프로그램 사이의 상호 작용을 분석해요.
외부 요소가 언제 프로그램을 사용하는지 혹은 프로그램이 외부 요소를 언제 사용하는지 파악하죠.
그리고 분석 결과를 유즈케이스(Usecase) 다이어그램으로 작성할거예요.

설계 단계에서는 역할에 따라 사용자 정의 형식을 결정하고 형식 간의 관계를 정의해요.
그리고 유크케이스 별로 시퀀스를 정의하죠.
사용자 정의 형식과 형식 간의 관계는 클래스 다이어그램으로 표현할 거예요.
그리고 시퀀스를 정의한 것은 시퀀스(Sequence) 다이어그램으로 만들기로 해요.

C언어로 프로그램을 개발할 때 이러한 개발 공정이 맞는 것인지 의구심이 들 수도 있어요.
여기서는 C++이나 Java, C# 언어로 비지니스 프로그래밍에 자주 사용하는 CBD 개발 방법론의 일부 단계를 접목할 거예요.

참고로 다이어그램을 작성하는 도구를 CASE 도구라 말해요.
여러 종류의 제품이 있는데 StarUml은 무료로 배포하는 오픈 프로젝트의 결과물이어서 쉽게 다운받아 사용할 수 있어요.
다음은 실습할 장르별 도서 관리 프로그램 시나리오예요.

 프로젝트 명: 장르별 도서 관리 프로그램

▶ 개요
장르별 도서 관리 프로그램은 콘솔에서 동작하는 응용 프로그램입니다.

 프로그램 흐름
프로그램을 시작하면 초기 작업으로 파일에 저장한 정보를 로딩합니다.
만약 파일이 없거나 비어있을 때는 초기 작업은 생략합니다.

초기 작업을 수행 후에는 최종 사용자에게 메뉴를 선택하게 하여 선택한 기능을 수행하는 것을 반복합니다.
메뉴에는 장르 추가, 장르 삭제, 전체 장르 보기, 특정 장르의 도서 목록 보기, 도서 추가, 도서 일련 번호로 검색, 도서 이름으로 검색, 전체 도서  보기, 프로그램 종료를 선택할 수 있습니다.
만약 최종 사용자가 프로그램 종료를 선택하면 상호 작용을 끝냅니다.

최종 사용자와 상호 작용이 끝나면 모든 장르 정보와 도서 정보를 파일에 저장하고 기존에 파일에 있던 내용에 덮어 씁니다.

장르 추가에서는 같은 이름의 장르는 추가하지 않게 합시다.
그리고 장르를 삭제할 때 장르 내 도서들도 같이 삭제합니다.
도서 추가에서는 장르를 선택한 후에 도서를 추가합니다.
도서의 일련 번호는 장르에 추가한 도서의 번호로 특정 장르 내에서 도서를 구분하는 키입니다.
도서 일련 번호로 검색 기능은 최종 사용자가 장르를 선택한 후에 도서 일련 번호를 입력하여 검색합니다.
도서 이름으로 검색 기능에서는 같은 이름의 모든 도서 정보를 출력합니다.
전체 도서 보기에서는 모든 장르와 장르 내 모든 도서 정보를 출력합니다.

 장르 정보
장르 정보에는 장르 일련 번호와 장르 이름과 장르 내에 보관한 도서 수가 있습니다.

 도서 정보
도서 정보에는 도서 일련 번호, 도서명, 저자 이름이 있습니다.
그리고 도서 일련 번호는 장르 내에 도서를 추가할 때 순서대로 부여하는 번호입니다.

102. 도서관리 프로그램-요구분석 및 정의

C언어 도서 관리 프로로그램 - 요구 분석 및 정의, 코드로 변환

요구 분석 및 정의 단계에서는 프로젝트와 이해 관계가 있는 이해 관계자를 파악하는 것에서 출발해요.
그리고 이해 관계자 별로 요구사항을 수집하는데 여기서는 생략할게요.

요구 사항을 수집한 후에는 작성할 시스템(여기서는 프로그램)을 사용할 사용자와 외부 시스템, 그리고 작성할 시스템이 사용하는 다른 외부 시스템이 있는지 조사해요.
이처럼 작성할 시스템과 상호작용하는 사용자와 외부 시스템을 액터라 불러요.
그리고 언제 우리 시스템을 사용하고 우리 시스템이 언제 다른 시스템을 사용하는지 조사하죠.
이러한 작업을 수행한 후에 결과를 유즈케이스 다이어그램으로 나타내죠.

유즈 케이스 다이어그램은 크게 액터와 유즈 케이스, 관계로 분류할 수 있어요.
액터는 작성할 시스템과 상호작용하는 외부 요소들이예요.
여기서는 최종 사용자와 파일 시스템을 액터라 할 수 있겠죠.
이 프로그램에서는 초기화 부분과 사용자와 상호 작용하는 부분, 해제화 부분이 있겠죠.

유즈케이스
유즈케이스

상호작용하는 부분에서 최종 사용자가 선택할 수 있는 기능에는 장르 추가, 장르 삭제, 전체 장르 보기, 특정 장르의 도서 목록 보기, 도서 추가, 도서 일련 번호로 검색, 도서 이름으로 검색, 전체 도서 보기가 있죠.

도서 관리 프로그램 유즈케이스
도서 관리 프로그램 유즈케이스

103. 도서관리프로그램-분석결과를 코드로

실습 각 단계에서 다이어그램을 작성할 때마다 약속한 부분을 코드로 구현할게요.
먼저 Win32 콘솔 응용 프로그램 프로젝트를 생성한 후에 Program.h 파일과 Program.c 파일을 추가하세요.
그리고 앞에서 작성한 동적 배열(Part 24. 사용자 정의 동적 배열 만들기)을 추가하세요.

요구 분석한 부분을 코드로 옮기기 전에 콘솔 응용 프로그램 종류에 관계없이 사용할  두 개의 함수를 작성하고 출발합시다.
하나는 콘솔 화면을 지우는 함수예요.

void clrscr();

그리고 나머지 하나는 기능 키를 입력받는 함수예요.

int getkey();

콘솔 화면을 지우는 함수는 단순히 system(“cls”)를 래핑한 함수예요.

void clrscr()
{
    system("cls");
}

기능 키를 입력받는 함수는 키보드의 F1, F2, F3 등의 키를 입력받는 함수예요.
ASCII 코드에는 기능 키(F1, F2, …)를 정의하지 않았어요.
ASCII 코드를 정의할 당시에는 키보드에 기능 키가 없었거든요.
conio.h 파일을 추가하여 getch 함수나 getche 함수를 이용하면 기능 키를 입력받을 수 있어요.
getch 함수는 누른 키를 콘솔 화면에 표시하지 않고 getche 함수는 표시하는 점을 빼고는 동작 원리는 같아요.
여기서는 getch 함수를 사용할게요.

getchar 함수는 최종 사용자가 엔터를 눌러야 입력한 문자들 중에 맨 앞에 문자의 아스키 코드 값을 반환했죠.
하지만 getch 함수는 getchar 함수와 달리 키보드를 누르면 바로 반응해요.
기능 키를 눌렀을 때 어떤 키를 눌렀는지 확인하려면 getch 함수를 두 번 호출해야 확인할 수 있어요.
getch 함수를 호출하였을 때 최종 사용자가 F1~F10 사이의 기능 키를 누르면 0을 반환해요.
그리고 다시 getch 함수를 호출하면 어떤 기능 키를 눌렀는지 확인할 수 있어요.
59는 F1, 60은 F2, … 예요.
마지막으로 getch 함수를 호출하였을 때 ESC 키를 누르면 27을 반환해요.
먼저 기능 키를 열거형으로 정의하세요.
여기서는 다른 키를 눌렀을 때의 값을 NO_DEFINED로 정하고 ESC, F1, F2 순으로 열거하기로 해요.

typedef enum _key key;
enum _key
{
    NO_DEFINED, F1, F2, F3, F4, F5, F6, F7, F8, F9,F10, ESC
};

최종 사용자가 어떠한 기능 키를 입력했는지 확인하는 함수를 getkey라고 정할게요.

int getkey(void); //getkey: 최종 사용자가 입력한 기능 키를 반환하는 함수

◈ ehcommon.h

#pragma once
typedef enum _key key;
enum _key
{
    NO_DEFINED, F1, F2, F3, F4, F5, F6, F7, F8, F9,F10, ESC
};
void clrscr();
int getkey();

◈ ehcommon.c

#pragma warning(disable:4996)
#include "ehcommon.h"
#include <conio.h>
#include <stdio.h>
#include <process.h>
void clrscr()
{
    system("cls");
}
 
int getkey()
{
    int key = 0;
    key = getch();
    if(key == 27)
    {
        return ESC;
    }
    if(key == 0)
    {
        key = getch();
        switch(key)
        {
        case 59: return F1;case 60: return F2;case 61: return F3;case 62: return F4;
        case 63: return F5;case 64: return F6;case 65: return F7;case 66: return F8;
        case 67: return F9; case 68: return F10;
        }
    }
    return NO_DEFINED;
}  

프로그램은 시작하면서 App 개체를 생성하고 사용자와 상호작용을 수행한 후에 App 개체를 소멸하는 순서로 수행하게 하세요.

App 개체를 생성할 때는 데이터 파일이 있을 때 파일의 내용으로 데이터를 로딩하는 작업을 수행하세요.
이를 위해 Program.c 파일에 초기 파일 이름을 정의하세요.
여기서 정한 파일 이름과 확장자는 여러분께서 적당하게 정하세요.
#define DEF_FNAME    “member.ehd”

그리고 main 함수로 전달받은 인자가 2개일 때는 프로그램에 사용할 파일 이름으로 두 번째 인자를 사용할게요.
◈ Program.c

#include "App.h"
#define DEF_FNAME    "member.ehd"
int main(int argc, char **argv)
{
    App *app = 0;
    if(argc != 2)
    {
        app = NewApp(DEF_FNAME);
    }
    else
    {
        app = NewApp(argv[1]);
    }
    AppRun(app);
    DeleteApp(app);
    return 0;
}

설계 단계에서 프로그램에 사용할 형식을 정의할 거예요.
하지만 여기에서 기본적인 흐름을 정의하기 위해 간단하게 App 구조체를 가상으로 정의하세요.
그리고 App을 동적으로 생성하는 함수와 소멸하는 함수, 사용자와 상호 작용하는 함수 Run을 선언하세요.

◈ App.h

#pragma once
typedef struct _App    App;
#include <stdio.h>
struct _App
{
    char fname[FILENAME_MAX];
};
 
App *NewApp(const char *fname);
void AppRun(App *app);
void DeleteApp(App *app

이제 App.c 파일을 작성하기로 해요.

먼저 App 데이터를 동적으로 생성하는 함수를 작성하세요.
App 형식 크기의 메모리를 할당받아요.
그리고 할당받은 App 데이터의 생성 시 해야 할 초기 작업을 수행하고고 할당받은 메모리를 반환하세요.

App *NewApp(const char *fname)
{
   App *app = 0;
   app = (App *)malloc(sizeof(App));
   AppApp(app,fname);
   return app;
}

App 데이터를 생성한 후에 초기화하는 함수에서는 인자로 전달받은 파일 이름을 설정한 후에 파일에 보관한 데이터를 로딩하는 작업을 수행하세요.

void AppApp(App *app,const char *fname)
{
   memset(app->fname,0,sizeof(app->fname));
   strncpy(app->fname,fname,FILENAME_MAX);
   AppLoad(app);
   printf("아무 키나 누르세요.\n");
   getkey();
}

AppLoad 함수는 설계 및 구현 단계에서 해야 할 일을 고민하고 작성하기로 해요.
App 개체를 소멸하는 함수에서는 관리하는 데이터를 저장한 후에 소멸하기 전에 해제하고 자신의 메모리를 해제하게 하세요.

void DeleteApp(App *app)
{
   AppSave(app);
   AppTApp(app);
   free(app);
}

AppSave 함수와 AppTApp 함수는 설계 및 구현 단계에서 해야 할 일을 고민하고 구현하기로 해요.

최종 사용자와 상호작용하는 함수에서는 메뉴를 선택하면 선택한 기능을 반복할 거예요.

void AppRun(App *app)
{
    int key = 0;
    while((key = AppSelectMenu(app))!=ESC)
    {
        switch(key)
        {
            case F1: AppAddGenre(app); break;
            case F2: AppRemoveGenre(app); break;
            case F3: AppListGenre(app); break;
            case F4: AppListBookAtGenre(app); break;
            case F5: AppAddBook(app); break;
            case F6: AppFindBookByNum(app); break;
            case F7: AppFindBookByTitle(app); break;
            case F8: AppListAll(app); break;
            default: printf("잘못 선택하였습니다.\n"); break;
        }
        printf("아무 키나 누르세요.\n");
        getkey();
    }
}

◈ App.c

#pragma warning(disable:4996)
#include "App.h"
#include "ehcommon.h"
#include <malloc.h>
#include <stdio.h>
#include <memory.h>
#include <string.h>
 
void AppApp(App *app,const char *fname);
void AppTApp(App *app);
void AppLoad(App *app);
void AppSave(App *app);
 
int AppSelectMenu(App *app);
void AppAddGenre(App *app);
void AppRemoveGenre(App *app);
void AppListGenre(App *app);
void AppListBookAtGenre(App *app); 
void AppAddBook(App *app); 
void AppFindBookByNum(App *app); 
void AppFindBookByTitle(App *app); 
void AppListAll(App *app); 
 
App *NewApp(const char *fname)
{
    App *app = 0;
    app = (App *)malloc(sizeof(App));
    AppApp(app,fname);
    return app;
}
 
void AppApp(App *app,const char *fname)
{
    memset(app->fname,0,sizeof(app->fname));
    strncpy(app->fname,fname,FILENAME_MAX);
    AppLoad(app);
    printf("아무 키나 누르세요.\n");
    getkey();
}
void AppLoad(App *app)
{
    printf("Load\n");    
}
void AppRun(App *app)
{
    int key = 0;
    while((key = AppSelectMenu(app))!=ESC)
    {
        switch(key)
        {
        case F1: AppAddGenre(app); break;
        case F2: AppRemoveGenre(app); break;
        case F3: AppListGenre(app); break;
        case F4: AppListBookAtGenre(app); break;
        case F5: AppAddBook(app); break;
        case F6: AppFindBookByNum(app); break;
        case F7: AppFindBookByTitle(app); break;
        case F8: AppListAll(app); break;
        default: printf("잘못 선택하였습니다.\n"); break;
        }
        printf("아무 키나 누르세요.\n");
        getkey();
    }
} 
int AppSelectMenu(App *app)
{
    clrscr();
    printf("장르별 도서관리 프로그램 \n");
    printf("F1:장르 추가 F2: 장르 삭제 F3: 전체 장르 보기\n");
    printf("F4: 특정 장르의 도서 목록 보기\n");
    printf("F5:도서 추가 F6: 도서 검색(일련번호) F7: 도서검색(제목)\n");
    printf("F8: 전체 도서 보기 ESC: 종료\n");
    return getkey();
}
void AppAddGenre(App *app)
{
    printf("장르 추가\n");    
}
void AppRemoveGenre(App *app)
{
    printf("장르 삭제\n");    
}
void AppListGenre(App *app)
{
    printf("전체 장르 보기\n");    
}
void AppListBookAtGenre(App *app)
{
    printf("특정 장르 내 도서 목록 보기\n");    
}
void AppAddBook(App *app)
{
    printf("도서 추가\n");    
}
void AppFindBookByNum(App *app)
{
    printf("도서 번호로 도서 검색\n");    
}
void AppFindBookByTitle(App *app)
{
    printf("도서 제목으로 도서 검색\n");    
}
void AppListAll(App *app)
{
    printf("전체 보기\n");    
}
void DeleteApp(App *app)
{
    AppSave(app);
    AppTApp(app);
    free(app);
}
void AppTApp(App *app)
{
    printf("App 해제화\n");     
}
void AppSave(App *app)
{
    printf("Save\n");    
}

여러분은 주기적으로 프로젝트를 빌드해서 오류가 있는지 확인하고 오류를 잡으면서 작업을 진행하세요.